refactor(ui): improve message input controller
This commit is contained in:
@@ -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);
|
||||
|
||||
+110
-192
@@ -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();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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');
|
||||
+3
-3
@@ -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),
|
||||
+2
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user