refactor(ui): improve message input controller

This commit is contained in:
Salvatore Giordano
2021-12-01 10:52:45 +01:00
parent 04f9208273
commit 0b0ff753d4
14 changed files with 241 additions and 241 deletions
@@ -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<String, dynamic> json) => _$MessageFromJson(
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
).copyWith(
status: MessageSendingStatus.sent,
);
/// The message ID. This is either created by Stream or set client side when
@@ -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,
@@ -126,7 +126,9 @@ class ThreadPage extends StatelessWidget {
),
),
MessageInput(
parentMessage: parent,
messageInputController: MessageInputController(
message: Message(parentId: parent!.id),
),
),
],
),
@@ -166,7 +166,9 @@ class ThreadPage extends StatelessWidget {
),
),
MessageInput(
parentMessage: parent,
messageInputController: MessageInputController(
message: Message(parentId: parent!.id),
),
),
],
),
@@ -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:
@@ -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'
@@ -610,7 +610,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
widget.editMessageInputBuilder!(context, widget.message)
else
MessageInput(
editMessage: widget.message,
messageInputController: MessageInputController(
message: widget.message,
),
preMessageSending: (m) {
FocusScope.of(context).unfocus();
Navigator.pop(context);
@@ -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<OverlayOptions> 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<Message> 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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
Widget build(BuildContext context) {
Widget child = ValueListenableBuilder<Message>(
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<MessageInput> {
: _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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
height: 24,
width: 24,
),
onPressed: () {
setState(() => _commandEnabled = false);
},
onPressed: messageInputController.clear,
),
),
if (!_commandEnabled &&
@@ -848,8 +832,10 @@ class MessageInputState extends State<MessageInput> {
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<MessageInput> {
);
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<MessageInput> {
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<MessageInput> {
void _chooseEmoji(List<String> 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<MessageInput> {
(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<MessageInput> {
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<MessageInput> {
///
/// 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<MessageInput> {
}
}
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<void> 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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
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<MessageInput> {
);
}
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<MessageInput> {
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();
}
}
@@ -116,8 +116,8 @@ class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
@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');
@@ -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),
@@ -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';
@@ -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';
@@ -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';
@@ -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<Message> {
/// 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<Attachment> attachments,
) =>
MessageInputController._(Message(attachments: attachments));
List<Attachment> 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<Message> {
///
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<Message> {
_textEditingController.clear();
}
/// Set the [value] to the initial [Message] value.
void reset() => value = _initialMessage;
@override
void dispose() {
super.dispose();
removeListener(_textEditingSyncer);
_textEditingController.dispose();
}
}