added basic replacements for controller
This commit is contained in:
@@ -178,7 +178,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.keyboardType = TextInputType.multiline,
|
this.keyboardType = TextInputType.multiline,
|
||||||
this.disableAttachments = false,
|
this.disableAttachments = false,
|
||||||
this.initialMessage,
|
this.initialMessage,
|
||||||
this.textEditingController,
|
this.messageInputController,
|
||||||
this.actions = const [],
|
this.actions = const [],
|
||||||
this.actionsLocation = ActionsLocation.left,
|
this.actionsLocation = ActionsLocation.left,
|
||||||
this.attachmentThumbnailBuilders,
|
this.attachmentThumbnailBuilders,
|
||||||
@@ -256,7 +256,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
final bool hideSendAsDm;
|
final bool hideSendAsDm;
|
||||||
|
|
||||||
/// The text controller of the TextField
|
/// The text controller of the TextField
|
||||||
final TextEditingController? textEditingController;
|
final MessageInputController? messageInputController;
|
||||||
|
|
||||||
/// List of action widgets
|
/// List of action widgets
|
||||||
final List<Widget> actions;
|
final List<Widget> actions;
|
||||||
@@ -339,9 +339,6 @@ class MessageInput extends StatefulWidget {
|
|||||||
|
|
||||||
/// State of [MessageInput]
|
/// State of [MessageInput]
|
||||||
class MessageInputState extends State<MessageInput> {
|
class MessageInputState extends State<MessageInput> {
|
||||||
final _attachments = <String, Attachment>{};
|
|
||||||
final List<User> _mentionedUsers = [];
|
|
||||||
|
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
late final _focusNode = widget.focusNode ?? FocusNode();
|
late final _focusNode = widget.focusNode ?? FocusNode();
|
||||||
bool _inputEnabled = true;
|
bool _inputEnabled = true;
|
||||||
@@ -356,15 +353,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
int _filePickerIndex = 0;
|
int _filePickerIndex = 0;
|
||||||
|
|
||||||
/// The editing controller passed to the input TextField
|
/// The editing controller passed to the input TextField
|
||||||
late final TextEditingController textEditingController =
|
late final MessageInputController messageInputController =
|
||||||
widget.textEditingController ?? TextEditingController();
|
widget.messageInputController ?? MessageInputController();
|
||||||
|
|
||||||
late StreamChatThemeData _streamChatTheme;
|
late StreamChatThemeData _streamChatTheme;
|
||||||
late MessageInputThemeData _messageInputTheme;
|
late MessageInputThemeData _messageInputTheme;
|
||||||
|
|
||||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||||
|
|
||||||
bool get _messageIsPresent => textEditingController.text.trim().isNotEmpty;
|
bool get _messageIsPresent => messageInputController.text.trim().isNotEmpty;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -372,7 +369,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
if (widget.editMessage != null || widget.initialMessage != null) {
|
if (widget.editMessage != null || widget.initialMessage != null) {
|
||||||
_parseExistingMessage(widget.editMessage ?? widget.initialMessage!);
|
_parseExistingMessage(widget.editMessage ?? widget.initialMessage!);
|
||||||
}
|
}
|
||||||
textEditingController.addListener(_onChangedDebounced);
|
messageInputController.textEditingController
|
||||||
|
.addListener(_onChangedDebounced);
|
||||||
_focusNode.addListener(_focusNodeListener);
|
_focusNode.addListener(_focusNodeListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,64 +412,67 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget child = DecoratedBox(
|
Widget child = ValueListenableBuilder<Message>(
|
||||||
decoration: BoxDecoration(
|
valueListenable: messageInputController,
|
||||||
color: _messageInputTheme.inputBackgroundColor,
|
builder: (context, value, wid) => DecoratedBox(
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
child: SafeArea(
|
color: _messageInputTheme.inputBackgroundColor,
|
||||||
child: GestureDetector(
|
),
|
||||||
onPanUpdate: (details) {
|
child: SafeArea(
|
||||||
if (details.delta.dy > 0) {
|
child: GestureDetector(
|
||||||
_focusNode.unfocus();
|
onPanUpdate: (details) {
|
||||||
if (_openFilePickerSection) {
|
if (details.delta.dy > 0) {
|
||||||
setState(() {
|
_focusNode.unfocus();
|
||||||
_openFilePickerSection = false;
|
if (_openFilePickerSection) {
|
||||||
});
|
setState(() {
|
||||||
|
_openFilePickerSection = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
child: Column(
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
if (_hasQuotedMessage)
|
||||||
if (_hasQuotedMessage)
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
child: Row(
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.all(8),
|
||||||
padding: const EdgeInsets.all(8),
|
child: StreamSvgIcon.reply(
|
||||||
child: StreamSvgIcon.reply(
|
color: _streamChatTheme.colorTheme.disabled,
|
||||||
color: _streamChatTheme.colorTheme.disabled,
|
),
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
Text(
|
context.translations.replyToMessageLabel,
|
||||||
context.translations.replyToMessageLabel,
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
),
|
||||||
),
|
IconButton(
|
||||||
IconButton(
|
visualDensity: VisualDensity.compact,
|
||||||
visualDensity: VisualDensity.compact,
|
icon: StreamSvgIcon.closeSmall(),
|
||||||
icon: StreamSvgIcon.closeSmall(),
|
onPressed: widget.onQuotedMessageCleared,
|
||||||
onPressed: widget.onQuotedMessageCleared,
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: _buildTextField(context),
|
|
||||||
),
|
|
||||||
if (widget.parentMessage != null && !widget.hideSendAsDm)
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
right: 12,
|
child: _buildTextField(context),
|
||||||
left: 12,
|
|
||||||
bottom: 12,
|
|
||||||
),
|
|
||||||
child: _buildDmCheckbox(),
|
|
||||||
),
|
),
|
||||||
_buildFilePickerSection(),
|
if (widget.parentMessage != null && !widget.hideSendAsDm)
|
||||||
],
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 12,
|
||||||
|
left: 12,
|
||||||
|
bottom: 12,
|
||||||
|
),
|
||||||
|
child: _buildDmCheckbox(),
|
||||||
|
),
|
||||||
|
_buildFilePickerSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -493,12 +494,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
OverlayOptions(
|
OverlayOptions(
|
||||||
visible: _focusNode.hasFocus &&
|
visible: _focusNode.hasFocus &&
|
||||||
textEditingController.text.isNotEmpty &&
|
messageInputController.text.isNotEmpty &&
|
||||||
textEditingController.selection.baseOffset > 0 &&
|
messageInputController.baseOffset > 0 &&
|
||||||
textEditingController.text
|
messageInputController.text
|
||||||
.substring(
|
.substring(
|
||||||
0,
|
0,
|
||||||
textEditingController.selection.baseOffset,
|
messageInputController.baseOffset,
|
||||||
)
|
)
|
||||||
.contains(':'),
|
.contains(':'),
|
||||||
widget: _buildEmojiOverlay(),
|
widget: _buildEmojiOverlay(),
|
||||||
@@ -591,7 +592,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
late Widget sendButton;
|
late Widget sendButton;
|
||||||
if (_timeOut > 0) {
|
if (_timeOut > 0) {
|
||||||
sendButton = _CountdownButton(count: _timeOut);
|
sendButton = _CountdownButton(count: _timeOut);
|
||||||
} else if (!_messageIsPresent && _attachments.isEmpty) {
|
} else if (!_messageIsPresent &&
|
||||||
|
messageInputController.attachments.isEmpty) {
|
||||||
sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
|
sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
|
||||||
} else {
|
} else {
|
||||||
sendButton = widget.activeSendButton != null
|
sendButton = widget.activeSendButton != null
|
||||||
@@ -700,7 +702,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
maxLines: null,
|
maxLines: null,
|
||||||
onSubmitted: (_) => sendMessage(),
|
onSubmitted: (_) => sendMessage(),
|
||||||
keyboardType: widget.keyboardType,
|
keyboardType: widget.keyboardType,
|
||||||
controller: textEditingController,
|
controller: messageInputController.textEditingController,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
style: _messageInputTheme.inputTextStyle,
|
style: _messageInputTheme.inputTextStyle,
|
||||||
autofocus: widget.autofocus,
|
autofocus: widget.autofocus,
|
||||||
@@ -823,7 +825,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
late final _onChangedDebounced = debounce(
|
late final _onChangedDebounced = debounce(
|
||||||
() {
|
() {
|
||||||
var value = textEditingController.text;
|
var value = messageInputController.text;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
|
|
||||||
@@ -853,7 +855,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
if (_commandEnabled && _chosenCommand!.name == 'giphy') {
|
if (_commandEnabled && _chosenCommand!.name == 'giphy') {
|
||||||
return context.translations.searchGifLabel;
|
return context.translations.searchGifLabel;
|
||||||
}
|
}
|
||||||
if (_attachments.isNotEmpty) {
|
if (messageInputController.attachments.isNotEmpty) {
|
||||||
return context.translations.addACommentOrSendLabel;
|
return context.translations.addACommentOrSendLabel;
|
||||||
}
|
}
|
||||||
if (_timeOut != 0) {
|
if (_timeOut != 0) {
|
||||||
@@ -865,12 +867,17 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
void _checkEmoji(String s, BuildContext context) {
|
void _checkEmoji(String s, BuildContext context) {
|
||||||
if (s.isNotEmpty &&
|
if (s.isNotEmpty &&
|
||||||
textEditingController.selection.baseOffset > 0 &&
|
messageInputController.baseOffset > 0 &&
|
||||||
textEditingController.text
|
messageInputController.text
|
||||||
.substring(0, textEditingController.selection.baseOffset)
|
.substring(
|
||||||
|
0,
|
||||||
|
messageInputController.baseOffset,
|
||||||
|
)
|
||||||
.contains(':')) {
|
.contains(':')) {
|
||||||
final textToSelection = textEditingController.text
|
final textToSelection = messageInputController.text.substring(
|
||||||
.substring(0, textEditingController.value.selection.start);
|
0,
|
||||||
|
messageInputController.textEditingController.value.selection.start,
|
||||||
|
);
|
||||||
final splits = textToSelection.split(':');
|
final splits = textToSelection.split(':');
|
||||||
final query = splits[splits.length - 2].toLowerCase();
|
final query = splits[splits.length - 2].toLowerCase();
|
||||||
final emoji = Emoji.byName(query);
|
final emoji = Emoji.byName(query);
|
||||||
@@ -883,9 +890,12 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
void _checkMentions(String s, BuildContext context) {
|
void _checkMentions(String s, BuildContext context) {
|
||||||
if (s.isNotEmpty &&
|
if (s.isNotEmpty &&
|
||||||
textEditingController.selection.baseOffset > 0 &&
|
messageInputController.baseOffset > 0 &&
|
||||||
textEditingController.text
|
messageInputController.text
|
||||||
.substring(0, textEditingController.selection.baseOffset)
|
.substring(
|
||||||
|
0,
|
||||||
|
messageInputController.baseOffset,
|
||||||
|
)
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.last
|
.last
|
||||||
.contains('@')) {
|
.contains('@')) {
|
||||||
@@ -921,7 +931,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommandsOverlayEntry() {
|
Widget _buildCommandsOverlayEntry() {
|
||||||
final text = textEditingController.text.trimLeft();
|
final text = messageInputController.text.trimLeft();
|
||||||
|
|
||||||
final renderObject = context.findRenderObject() as RenderBox?;
|
final renderObject = context.findRenderObject() as RenderBox?;
|
||||||
if (renderObject == null) {
|
if (renderObject == null) {
|
||||||
@@ -937,16 +947,16 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
Widget _buildFilePickerSection() {
|
Widget _buildFilePickerSection() {
|
||||||
final _attachmentContainsFile =
|
final _attachmentContainsFile =
|
||||||
_attachments.values.any((it) => it.type == 'file');
|
messageInputController.attachments.any((it) => it.type == 'file');
|
||||||
|
|
||||||
final attachmentLimitCrossed =
|
final attachmentLimitCrossed =
|
||||||
_attachments.length >= widget.attachmentLimit;
|
messageInputController.attachments.length >= widget.attachmentLimit;
|
||||||
|
|
||||||
Color _getIconColor(int index) {
|
Color _getIconColor(int index) {
|
||||||
final streamChatThemeData = _streamChatTheme;
|
final streamChatThemeData = _streamChatTheme;
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
return _attachments.isEmpty
|
return messageInputController.attachments.isEmpty
|
||||||
? streamChatThemeData.colorTheme.accentPrimary
|
? streamChatThemeData.colorTheme.accentPrimary
|
||||||
: (!_attachmentContainsFile
|
: (!_attachmentContainsFile
|
||||||
? streamChatThemeData.colorTheme.accentPrimary
|
? streamChatThemeData.colorTheme.accentPrimary
|
||||||
@@ -955,7 +965,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
case 1:
|
case 1:
|
||||||
return _attachmentContainsFile
|
return _attachmentContainsFile
|
||||||
? streamChatThemeData.colorTheme.accentPrimary
|
? streamChatThemeData.colorTheme.accentPrimary
|
||||||
: (_attachments.isEmpty
|
: (messageInputController.attachments.isEmpty
|
||||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||||
.withOpacity(0.5)
|
.withOpacity(0.5)
|
||||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||||
@@ -963,7 +973,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
case 2:
|
case 2:
|
||||||
return attachmentLimitCrossed
|
return attachmentLimitCrossed
|
||||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||||
: _attachmentContainsFile && _attachments.isNotEmpty
|
: _attachmentContainsFile &&
|
||||||
|
messageInputController.attachments.isNotEmpty
|
||||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||||
.withOpacity(0.2)
|
.withOpacity(0.2)
|
||||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||||
@@ -971,7 +982,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
case 3:
|
case 3:
|
||||||
return attachmentLimitCrossed
|
return attachmentLimitCrossed
|
||||||
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
|
||||||
: _attachmentContainsFile && _attachments.isNotEmpty
|
: _attachmentContainsFile &&
|
||||||
|
messageInputController.attachments.isNotEmpty
|
||||||
? streamChatThemeData.colorTheme.textHighEmphasis
|
? streamChatThemeData.colorTheme.textHighEmphasis
|
||||||
.withOpacity(0.2)
|
.withOpacity(0.2)
|
||||||
: streamChatThemeData.colorTheme.textHighEmphasis
|
: streamChatThemeData.colorTheme.textHighEmphasis
|
||||||
@@ -1001,26 +1013,26 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
icon: StreamSvgIcon.pictures(
|
icon: StreamSvgIcon.pictures(
|
||||||
color: _getIconColor(0),
|
color: _getIconColor(0),
|
||||||
),
|
),
|
||||||
onPressed:
|
onPressed: _attachmentContainsFile &&
|
||||||
_attachmentContainsFile && _attachments.isNotEmpty
|
messageInputController.attachments.isNotEmpty
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_filePickerIndex = 0;
|
_filePickerIndex = 0;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: 32,
|
iconSize: 32,
|
||||||
icon: StreamSvgIcon.files(
|
icon: StreamSvgIcon.files(
|
||||||
color: _getIconColor(1),
|
color: _getIconColor(1),
|
||||||
),
|
),
|
||||||
onPressed:
|
onPressed: !_attachmentContainsFile &&
|
||||||
!_attachmentContainsFile && _attachments.isNotEmpty
|
messageInputController.attachments.isNotEmpty
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
pickFile(DefaultAttachmentTypes.file);
|
pickFile(DefaultAttachmentTypes.file);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: StreamSvgIcon.camera(
|
icon: StreamSvgIcon.camera(
|
||||||
@@ -1028,7 +1040,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
onPressed: attachmentLimitCrossed ||
|
onPressed: attachmentLimitCrossed ||
|
||||||
(_attachmentContainsFile &&
|
(_attachmentContainsFile &&
|
||||||
_attachments.isNotEmpty)
|
messageInputController.attachments.isNotEmpty)
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
pickFile(
|
pickFile(
|
||||||
@@ -1044,7 +1056,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
),
|
),
|
||||||
onPressed: attachmentLimitCrossed ||
|
onPressed: attachmentLimitCrossed ||
|
||||||
(_attachmentContainsFile &&
|
(_attachmentContainsFile &&
|
||||||
_attachments.isNotEmpty)
|
messageInputController.attachments.isNotEmpty)
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
pickFile(
|
pickFile(
|
||||||
@@ -1088,11 +1100,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
filePickerIndex: _filePickerIndex,
|
filePickerIndex: _filePickerIndex,
|
||||||
streamChatTheme: _streamChatTheme,
|
streamChatTheme: _streamChatTheme,
|
||||||
containsFile: _attachmentContainsFile,
|
containsFile: _attachmentContainsFile,
|
||||||
selectedMedias: _attachments.keys.toList(),
|
selectedMedias: messageInputController.attachments
|
||||||
|
.map((e) => e.id)
|
||||||
|
.toList(),
|
||||||
onAddMoreFilesClick: pickFile,
|
onAddMoreFilesClick: pickFile,
|
||||||
onMediaSelected: (media) {
|
onMediaSelected: (media) {
|
||||||
if (_attachments.containsKey(media.id)) {
|
if (messageInputController.attachments
|
||||||
setState(() => _attachments.remove(media.id));
|
.any((e) => e.id == media.id)) {
|
||||||
|
setState(() => messageInputController.attachments
|
||||||
|
.removeWhere((e) => e.id == media.id));
|
||||||
} else {
|
} else {
|
||||||
_addAssetAttachment(media);
|
_addAssetAttachment(media);
|
||||||
}
|
}
|
||||||
@@ -1163,12 +1179,16 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMentionsOverlayEntry() {
|
Widget _buildMentionsOverlayEntry() {
|
||||||
if (textEditingController.value.selection.start < 0) {
|
if (messageInputController.textEditingController.value.selection.start <
|
||||||
|
0) {
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
final splits = textEditingController.text
|
final splits = messageInputController.text
|
||||||
.substring(0, textEditingController.value.selection.start)
|
.substring(
|
||||||
|
0,
|
||||||
|
messageInputController.textEditingController.value.selection.start,
|
||||||
|
)
|
||||||
.split('@');
|
.split('@');
|
||||||
final query = splits.last.toLowerCase();
|
final query = splits.last.toLowerCase();
|
||||||
|
|
||||||
@@ -1196,14 +1216,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
size: Size(renderObject.size.width - 16, 400),
|
size: Size(renderObject.size.width - 16, 400),
|
||||||
mentionsTileBuilder: tileBuilder,
|
mentionsTileBuilder: tileBuilder,
|
||||||
onMentionUserTap: (user) {
|
onMentionUserTap: (user) {
|
||||||
_mentionedUsers.add(user);
|
messageInputController.mentionedUsers.add(user);
|
||||||
splits[splits.length - 1] = user.name;
|
splits[splits.length - 1] = user.name;
|
||||||
final rejoin = splits.join('@');
|
final rejoin = splits.join('@');
|
||||||
|
|
||||||
textEditingController.value = TextEditingValue(
|
messageInputController.textEditingController.value = TextEditingValue(
|
||||||
text: rejoin +
|
text: rejoin +
|
||||||
textEditingController.text.substring(
|
messageInputController.text.substring(
|
||||||
textEditingController.selection.start,
|
messageInputController
|
||||||
|
.textEditingController.value.selection.start,
|
||||||
),
|
),
|
||||||
selection: TextSelection.collapsed(
|
selection: TextSelection.collapsed(
|
||||||
offset: rejoin.length,
|
offset: rejoin.length,
|
||||||
@@ -1216,12 +1237,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmojiOverlay() {
|
Widget _buildEmojiOverlay() {
|
||||||
if (textEditingController.value.selection.baseOffset < 0) {
|
if (messageInputController.baseOffset < 0) {
|
||||||
return const Offstage();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
final splits = textEditingController.text
|
final splits = messageInputController.text
|
||||||
.substring(0, textEditingController.value.selection.baseOffset)
|
.substring(
|
||||||
|
0,
|
||||||
|
messageInputController.baseOffset,
|
||||||
|
)
|
||||||
.split(':');
|
.split(':');
|
||||||
|
|
||||||
final query = splits.last.toLowerCase();
|
final query = splits.last.toLowerCase();
|
||||||
@@ -1240,10 +1264,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
void _chooseEmoji(List<String> splits, Emoji emoji) {
|
void _chooseEmoji(List<String> splits, Emoji emoji) {
|
||||||
final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!;
|
final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!;
|
||||||
|
|
||||||
textEditingController.value = TextEditingValue(
|
messageInputController.textEditingController.value = TextEditingValue(
|
||||||
text: rejoin +
|
text: rejoin +
|
||||||
textEditingController.text
|
messageInputController.text.substring(
|
||||||
.substring(textEditingController.selection.start),
|
messageInputController.textEditingController.selection.start,
|
||||||
|
),
|
||||||
selection: TextSelection.collapsed(
|
selection: TextSelection.collapsed(
|
||||||
offset: rejoin.length,
|
offset: rejoin.length,
|
||||||
),
|
),
|
||||||
@@ -1251,7 +1276,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _setCommand(Command c) {
|
void _setCommand(Command c) {
|
||||||
textEditingController.clear();
|
messageInputController.clear();
|
||||||
setState(() {
|
setState(() {
|
||||||
_chosenCommand = c;
|
_chosenCommand = c;
|
||||||
_commandEnabled = true;
|
_commandEnabled = true;
|
||||||
@@ -1273,11 +1298,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAttachments() {
|
Widget _buildAttachments() {
|
||||||
if (_attachments.isEmpty) return const Offstage();
|
if (messageInputController.attachments.isEmpty) return const Offstage();
|
||||||
final fileAttachments = _attachments.values
|
final fileAttachments = messageInputController.attachments
|
||||||
.where((it) => it.type == 'file')
|
.where((it) => it.type == 'file')
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
final remainingAttachments = _attachments.values
|
final remainingAttachments = messageInputController.attachments
|
||||||
.where((it) => it.type != 'file')
|
.where((it) => it.type != 'file')
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
return Column(
|
return Column(
|
||||||
@@ -1364,7 +1389,10 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
focusElevation: 0,
|
focusElevation: 0,
|
||||||
hoverElevation: 0,
|
hoverElevation: 0,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _attachments.remove(attachment.id));
|
setState(
|
||||||
|
() => messageInputController.attachments
|
||||||
|
.removeWhere((e) => e.id == attachment.id),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
fillColor:
|
fillColor:
|
||||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||||
@@ -1443,7 +1471,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommandButton(BuildContext context) {
|
Widget _buildCommandButton(BuildContext context) {
|
||||||
final s = textEditingController.text.trim();
|
final s = messageInputController.text.trim();
|
||||||
final defaultButton = IconButton(
|
final defaultButton = IconButton(
|
||||||
icon: StreamSvgIcon.lightning(
|
icon: StreamSvgIcon.lightning(
|
||||||
color: s.isNotEmpty
|
color: s.isNotEmpty
|
||||||
@@ -1574,10 +1602,11 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
setState(() => _addAttachments([attachment]));
|
setState(() => _addAttachments([attachment]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds an attachment to the [_attachments] map
|
/// Adds an attachment to the [messageInputController.attachments] map
|
||||||
void _addAttachments(Iterable<Attachment> attachments) {
|
void _addAttachments(Iterable<Attachment> attachments) {
|
||||||
final limit = widget.attachmentLimit;
|
final limit = widget.attachmentLimit;
|
||||||
final length = _attachments.length + attachments.length;
|
final length =
|
||||||
|
messageInputController.attachments.length + attachments.length;
|
||||||
if (length > limit) {
|
if (length > limit) {
|
||||||
final onAttachmentLimitExceed = widget.onAttachmentLimitExceed;
|
final onAttachmentLimitExceed = widget.onAttachmentLimitExceed;
|
||||||
if (onAttachmentLimitExceed != null) {
|
if (onAttachmentLimitExceed != null) {
|
||||||
@@ -1591,7 +1620,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (final attachment in attachments) {
|
for (final attachment in attachments) {
|
||||||
_attachments[attachment.id] = attachment;
|
messageInputController.addAttachment(attachment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1754,8 +1783,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
/// Sends the current message
|
/// Sends the current message
|
||||||
Future<void> sendMessage() async {
|
Future<void> sendMessage() async {
|
||||||
var text = textEditingController.text.trim();
|
var text = messageInputController.text.trim();
|
||||||
if (text.isEmpty && _attachments.isEmpty) {
|
if (text.isEmpty && messageInputController.attachments.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1765,10 +1794,8 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
text = '${'/${_chosenCommand!.name} '}$text';
|
text = '${'/${_chosenCommand!.name} '}$text';
|
||||||
}
|
}
|
||||||
|
|
||||||
final attachments = [..._attachments.values];
|
messageInputController.clear();
|
||||||
|
messageInputController.attachments.clear();
|
||||||
textEditingController.clear();
|
|
||||||
_attachments.clear();
|
|
||||||
widget.onQuotedMessageCleared?.call();
|
widget.onQuotedMessageCleared?.call();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -1779,17 +1806,19 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
if (widget.editMessage != null) {
|
if (widget.editMessage != null) {
|
||||||
message = widget.editMessage!.copyWith(
|
message = widget.editMessage!.copyWith(
|
||||||
text: text,
|
text: text,
|
||||||
attachments: attachments,
|
attachments: messageInputController.attachments,
|
||||||
mentionedUsers:
|
mentionedUsers: messageInputController.mentionedUsers
|
||||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
.where((u) => text.contains('@${u.name}'))
|
||||||
|
.toList(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
message = (widget.initialMessage ?? Message()).copyWith(
|
message = (widget.initialMessage ?? Message()).copyWith(
|
||||||
parentId: widget.parentMessage?.id,
|
parentId: widget.parentMessage?.id,
|
||||||
text: text,
|
text: text,
|
||||||
attachments: attachments,
|
attachments: messageInputController.attachments,
|
||||||
mentionedUsers:
|
mentionedUsers: messageInputController.mentionedUsers
|
||||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
.where((u) => text.contains('@${u.name}'))
|
||||||
|
.toList(),
|
||||||
showInChannel: widget.parentMessage != null ? _sendAsDm : null,
|
showInChannel: widget.parentMessage != null ? _sendAsDm : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1810,7 +1839,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
await streamChannel.reloadChannel();
|
await streamChannel.reloadChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
_mentionedUsers.clear();
|
messageInputController.mentionedUsers.clear();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Future sendingFuture;
|
Future sendingFuture;
|
||||||
@@ -1909,13 +1938,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
|
|
||||||
void _parseExistingMessage(Message message) {
|
void _parseExistingMessage(Message message) {
|
||||||
final messageText = message.text;
|
final messageText = message.text;
|
||||||
if (messageText != null) textEditingController.text = messageText;
|
if (messageText != null) messageInputController.text = messageText;
|
||||||
_addAttachments(message.attachments);
|
_addAttachments(message.attachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
textEditingController.dispose();
|
messageInputController.textEditingController
|
||||||
|
.removeListener(_onChangedDebounced);
|
||||||
|
messageInputController.dispose();
|
||||||
_focusNode.removeListener(_focusNodeListener);
|
_focusNode.removeListener(_focusNodeListener);
|
||||||
_stopSlowMode();
|
_stopSlowMode();
|
||||||
_onChangedDebounced.cancel();
|
_onChangedDebounced.cancel();
|
||||||
|
|||||||
@@ -0,0 +1,782 @@
|
|||||||
|
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_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
|
export 'package:flutter/services.dart'
|
||||||
|
show
|
||||||
|
TextInputType,
|
||||||
|
TextInputAction,
|
||||||
|
TextCapitalization,
|
||||||
|
SmartQuotesType,
|
||||||
|
SmartDashesType;
|
||||||
|
|
||||||
|
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.
|
||||||
|
///
|
||||||
|
/// If [maxLengthEnforced] is set to false, then more than [maxLength]
|
||||||
|
/// characters may be entered, and the error counter and divider will
|
||||||
|
/// switch to the [decoration].errorStyle when the limit is exceeded.
|
||||||
|
///
|
||||||
|
/// 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],
|
||||||
|
/// [maxLengthEnforced], [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.0),
|
||||||
|
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(textAlign != null),
|
||||||
|
assert(readOnly != null),
|
||||||
|
assert(autofocus != null),
|
||||||
|
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
|
||||||
|
assert(obscureText != null),
|
||||||
|
assert(autocorrect != null),
|
||||||
|
smartDashesType = smartDashesType ??
|
||||||
|
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||||
|
smartQuotesType = smartQuotesType ??
|
||||||
|
(obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||||
|
assert(enableSuggestions != null),
|
||||||
|
assert(enableInteractiveSelection != null),
|
||||||
|
assert(maxLengthEnforced != null),
|
||||||
|
assert(
|
||||||
|
maxLengthEnforced || maxLengthEnforcement == null,
|
||||||
|
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
|
||||||
|
),
|
||||||
|
assert(scrollPadding != null),
|
||||||
|
assert(dragStartBehavior != null),
|
||||||
|
assert(selectionHeightStyle != null),
|
||||||
|
assert(selectionWidthStyle != null),
|
||||||
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(minLines == null || minLines > 0),
|
||||||
|
assert(
|
||||||
|
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||||
|
"minLines can't be greater than maxLines",
|
||||||
|
),
|
||||||
|
assert(expands != null),
|
||||||
|
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),
|
||||||
|
// 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.',
|
||||||
|
),
|
||||||
|
assert(enableIMEPersonalizedLearning != null),
|
||||||
|
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.
|
||||||
|
///
|
||||||
|
/// If [maxLengthEnforced] is set to false or [maxLengthEnforcement] is
|
||||||
|
/// [MaxLengthEnforcement.none], then more than [maxLength]
|
||||||
|
/// characters may be entered, but the error counter and divider will switch
|
||||||
|
/// to the [decoration]'s [InputDecoration.errorStyle] when the limit is
|
||||||
|
/// exceeded.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
|
||||||
|
final int? maxLength;
|
||||||
|
|
||||||
|
/// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to
|
||||||
|
/// enforce the limit, or merely provide a character counter and warning when
|
||||||
|
/// [maxLength] is exceeded.
|
||||||
|
///
|
||||||
|
/// If true, prevents the field from allowing more than [maxLength]
|
||||||
|
/// characters.
|
||||||
|
@Deprecated(
|
||||||
|
'Use maxLengthEnforcement parameter which provides more specific '
|
||||||
|
'behavior related to the maxLength limit. '
|
||||||
|
'This feature was deprecated after v1.25.0-5.0.pre.',
|
||||||
|
)
|
||||||
|
final bool maxLengthEnforced;
|
||||||
|
|
||||||
|
/// Determines how the [maxLength] limit should be enforced.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement}
|
||||||
|
///
|
||||||
|
/// {@macro flutter.services.textFormatter.maxLengthEnforcement}
|
||||||
|
final MaxLengthEnforcement? maxLengthEnforcement;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.onChanged}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [inputFormatters], which are called before [onChanged]
|
||||||
|
/// runs and can validate and change ("format") the input value.
|
||||||
|
/// * [onEditingComplete], [onSubmitted]:
|
||||||
|
/// which are more specialized input change notifications.
|
||||||
|
final ValueChanged<String>? onChanged;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.onEditingComplete}
|
||||||
|
final VoidCallback? onEditingComplete;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.onSubmitted}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [TextInputAction.next] and [TextInputAction.previous], which
|
||||||
|
/// automatically shift the focus to the next/previous focusable item when
|
||||||
|
/// the user is done editing.
|
||||||
|
final ValueChanged<String>? onSubmitted;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.onAppPrivateCommand}
|
||||||
|
final AppPrivateCommandCallback? onAppPrivateCommand;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.inputFormatters}
|
||||||
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
|
|
||||||
|
/// If false the text field is "disabled": it ignores taps and its
|
||||||
|
/// [decoration] is rendered in grey.
|
||||||
|
///
|
||||||
|
/// If non-null this property overrides the [decoration]'s
|
||||||
|
/// [InputDecoration.enabled] property.
|
||||||
|
final bool? enabled;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.cursorWidth}
|
||||||
|
final double cursorWidth;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.cursorHeight}
|
||||||
|
final double? cursorHeight;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.cursorRadius}
|
||||||
|
final Radius? cursorRadius;
|
||||||
|
|
||||||
|
/// The color of the cursor.
|
||||||
|
///
|
||||||
|
/// The cursor indicates the current location of text insertion point in
|
||||||
|
/// the field.
|
||||||
|
///
|
||||||
|
/// If this is null it will default to the ambient
|
||||||
|
/// [TextSelectionThemeData.cursorColor]. If that is null, and the
|
||||||
|
/// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS]
|
||||||
|
/// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use
|
||||||
|
/// the value of [ColorScheme.primary] of [ThemeData.colorScheme].
|
||||||
|
final Color? cursorColor;
|
||||||
|
|
||||||
|
/// Controls how tall the selection highlight boxes are computed to be.
|
||||||
|
///
|
||||||
|
/// See [ui.BoxHeightStyle] for details on available styles.
|
||||||
|
final ui.BoxHeightStyle selectionHeightStyle;
|
||||||
|
|
||||||
|
/// Controls how wide the selection highlight boxes are computed to be.
|
||||||
|
///
|
||||||
|
/// See [ui.BoxWidthStyle] for details on available styles.
|
||||||
|
final ui.BoxWidthStyle selectionWidthStyle;
|
||||||
|
|
||||||
|
/// The appearance of the keyboard.
|
||||||
|
///
|
||||||
|
/// This setting is only honored on iOS devices.
|
||||||
|
///
|
||||||
|
/// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
|
||||||
|
final Brightness? keyboardAppearance;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.scrollPadding}
|
||||||
|
final EdgeInsets scrollPadding;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
||||||
|
final bool enableInteractiveSelection;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.selectionControls}
|
||||||
|
final TextSelectionControls? selectionControls;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.selectionEnabled}
|
||||||
|
bool get selectionEnabled => enableInteractiveSelection;
|
||||||
|
|
||||||
|
/// {@template flutter.material.textfield.onTap}
|
||||||
|
/// Called for each distinct tap except for every second tap of a double tap.
|
||||||
|
///
|
||||||
|
/// The text field builds a [GestureDetector] to handle input events like tap,
|
||||||
|
/// to trigger focus requests, to move the caret, adjust the selection, etc.
|
||||||
|
/// Handling some of those events by wrapping the text field with a competing
|
||||||
|
/// GestureDetector is problematic.
|
||||||
|
///
|
||||||
|
/// To unconditionally handle taps, without interfering with the text field's
|
||||||
|
/// internal gesture detector, provide this callback.
|
||||||
|
///
|
||||||
|
/// If the text field is created with [enabled] false, taps will not be
|
||||||
|
/// recognized.
|
||||||
|
///
|
||||||
|
/// To be notified when the text field gains or loses the focus, provide a
|
||||||
|
/// [focusNode] and add a listener to that.
|
||||||
|
///
|
||||||
|
/// To listen to arbitrary pointer events without competing with the
|
||||||
|
/// text field's internal gesture detector, use a [Listener].
|
||||||
|
/// {@endtemplate}
|
||||||
|
final GestureTapCallback? onTap;
|
||||||
|
|
||||||
|
/// The cursor for a mouse pointer when it enters or is hovering over the
|
||||||
|
/// widget.
|
||||||
|
///
|
||||||
|
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
|
||||||
|
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
|
||||||
|
///
|
||||||
|
/// * [MaterialState.error].
|
||||||
|
/// * [MaterialState.hovered].
|
||||||
|
/// * [MaterialState.focused].
|
||||||
|
/// * [MaterialState.disabled].
|
||||||
|
///
|
||||||
|
/// If this property is null, [MaterialStateMouseCursor.textable] will be used.
|
||||||
|
///
|
||||||
|
/// The [mouseCursor] is the only property of [TextField] that controls the
|
||||||
|
/// appearance of the mouse pointer. All other properties related to "cursor"
|
||||||
|
/// stand for the text cursor, which is usually a blinking vertical line at
|
||||||
|
/// the editing position.
|
||||||
|
final MouseCursor? mouseCursor;
|
||||||
|
|
||||||
|
/// Callback that generates a custom [InputDecoration.counter] widget.
|
||||||
|
///
|
||||||
|
/// See [InputCounterWidgetBuilder] for an explanation of the passed in
|
||||||
|
/// arguments. The returned widget will be placed below the line in place of
|
||||||
|
/// the default widget built when [InputDecoration.counterText] is specified.
|
||||||
|
///
|
||||||
|
/// The returned widget will be wrapped in a [Semantics] widget for
|
||||||
|
/// accessibility, but it also needs to be accessible itself. For example,
|
||||||
|
/// if returning a Text widget, set the [Text.semanticsLabel] property.
|
||||||
|
///
|
||||||
|
/// {@tool snippet}
|
||||||
|
/// ```dart
|
||||||
|
/// Widget counter(
|
||||||
|
/// BuildContext context,
|
||||||
|
/// {
|
||||||
|
/// required int currentLength,
|
||||||
|
/// required int? maxLength,
|
||||||
|
/// required bool isFocused,
|
||||||
|
/// }
|
||||||
|
/// ) {
|
||||||
|
/// return Text(
|
||||||
|
/// '$currentLength of $maxLength characters',
|
||||||
|
/// semanticsLabel: 'character count',
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// If buildCounter returns null, then no counter and no Semantics widget will
|
||||||
|
/// be created at all.
|
||||||
|
final InputCounterWidgetBuilder? buildCounter;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.scrollPhysics}
|
||||||
|
final ScrollPhysics? scrollPhysics;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.scrollController}
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.autofillHints}
|
||||||
|
/// {@macro flutter.services.AutofillConfiguration.autofillHints}
|
||||||
|
final Iterable<String>? autofillHints;
|
||||||
|
|
||||||
|
/// {@template flutter.material.textfield.restorationId}
|
||||||
|
/// Restoration ID to save and restore the state of the text field.
|
||||||
|
///
|
||||||
|
/// If non-null, the text field will persist and restore its current scroll
|
||||||
|
/// offset and - if no [controller] has been provided - the content of the
|
||||||
|
/// text field. If a [controller] has been provided, it is the responsibility
|
||||||
|
/// of the owner of that controller to persist and restore it, e.g. by using
|
||||||
|
/// a [RestorableTextEditingController].
|
||||||
|
///
|
||||||
|
/// The state of this widget is persisted in a [RestorationBucket] claimed
|
||||||
|
/// from the surrounding [RestorationScope] using the provided restoration ID.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [RestorationManager], which explains how state restoration works in
|
||||||
|
/// Flutter.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final String? restorationId;
|
||||||
|
|
||||||
|
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
||||||
|
final bool enableIMEPersonalizedLearning;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_StreamMessageTextFieldState createState() => _StreamMessageTextFieldState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
|
||||||
|
defaultValue: null));
|
||||||
|
properties
|
||||||
|
.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<InputDecoration>(
|
||||||
|
'decoration', decoration,
|
||||||
|
defaultValue: const InputDecoration()));
|
||||||
|
properties.add(DiagnosticsProperty<TextInputType>(
|
||||||
|
'keyboardType', keyboardType,
|
||||||
|
defaultValue: TextInputType.text));
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
|
||||||
|
properties.add(DiagnosticsProperty<String>(
|
||||||
|
'obscuringCharacter', obscuringCharacter,
|
||||||
|
defaultValue: '•'));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText,
|
||||||
|
defaultValue: false));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect,
|
||||||
|
defaultValue: true));
|
||||||
|
properties.add(EnumProperty<SmartDashesType>(
|
||||||
|
'smartDashesType', smartDashesType,
|
||||||
|
defaultValue:
|
||||||
|
obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
|
||||||
|
properties.add(EnumProperty<SmartQuotesType>(
|
||||||
|
'smartQuotesType', smartQuotesType,
|
||||||
|
defaultValue:
|
||||||
|
obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
|
||||||
|
properties.add(DiagnosticsProperty<bool>(
|
||||||
|
'enableSuggestions', enableSuggestions,
|
||||||
|
defaultValue: true));
|
||||||
|
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
|
||||||
|
properties.add(IntProperty('minLines', minLines, defaultValue: null));
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||||
|
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
||||||
|
properties.add(FlagProperty('maxLengthEnforced',
|
||||||
|
value: maxLengthEnforced,
|
||||||
|
defaultValue: true,
|
||||||
|
ifFalse: 'maxLength not enforced'));
|
||||||
|
properties.add(EnumProperty<MaxLengthEnforcement>(
|
||||||
|
'maxLengthEnforcement', maxLengthEnforcement,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(EnumProperty<TextInputAction>(
|
||||||
|
'textInputAction', textInputAction,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(EnumProperty<TextCapitalization>(
|
||||||
|
'textCapitalization', textCapitalization,
|
||||||
|
defaultValue: TextCapitalization.none));
|
||||||
|
properties.add(EnumProperty<TextAlign>('textAlign', textAlign,
|
||||||
|
defaultValue: TextAlign.start));
|
||||||
|
properties.add(DiagnosticsProperty<TextAlignVertical>(
|
||||||
|
'textAlignVertical', textAlignVertical,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
|
||||||
|
defaultValue: null));
|
||||||
|
properties
|
||||||
|
.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
||||||
|
properties
|
||||||
|
.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius,
|
||||||
|
defaultValue: null));
|
||||||
|
properties
|
||||||
|
.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<Brightness>(
|
||||||
|
'keyboardAppearance', keyboardAppearance,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
|
||||||
|
'scrollPadding', scrollPadding,
|
||||||
|
defaultValue: const EdgeInsets.all(20.0)));
|
||||||
|
properties.add(FlagProperty('selectionEnabled',
|
||||||
|
value: selectionEnabled,
|
||||||
|
defaultValue: true,
|
||||||
|
ifFalse: 'selection disabled'));
|
||||||
|
properties.add(DiagnosticsProperty<TextSelectionControls>(
|
||||||
|
'selectionControls', selectionControls,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<ScrollController>(
|
||||||
|
'scrollController', scrollController,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<ScrollPhysics>(
|
||||||
|
'scrollPhysics', scrollPhysics,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<bool>(
|
||||||
|
'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning,
|
||||||
|
defaultValue: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StreamMessageTextFieldState extends State<StreamMessageTextField>
|
||||||
|
with RestorationMixin<StreamMessageTextField> {
|
||||||
|
RestorableMessageInputController? _controller;
|
||||||
|
|
||||||
|
MessageInputController get _effectiveController =>
|
||||||
|
widget.controller ?? _controller!.value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.controller == null) {
|
||||||
|
_createLocalController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createLocalController([Message? message]) {
|
||||||
|
assert(_controller == null, '');
|
||||||
|
_controller = RestorableMessageInputController(message: message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant StreamMessageTextField oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.controller == null && oldWidget.controller != null) {
|
||||||
|
_createLocalController(oldWidget.controller!.value);
|
||||||
|
} else if (widget.controller != null && oldWidget.controller == null) {
|
||||||
|
unregisterFromRestoration(_controller!);
|
||||||
|
_controller!.dispose();
|
||||||
|
_controller = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||||
|
if (_controller != null) {
|
||||||
|
_registerController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get restorationId => widget.restorationId;
|
||||||
|
|
||||||
|
void _registerController() {
|
||||||
|
assert(_controller != null, '');
|
||||||
|
registerForRestoration(_controller!, 'controller');
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _onChangedDebounced = debounce(
|
||||||
|
(String newText) => _effectiveController.text = newText,
|
||||||
|
const Duration(milliseconds: 350),
|
||||||
|
leading: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => TextField(
|
||||||
|
key: widget.key,
|
||||||
|
controller: _effectiveController.textEditingController,
|
||||||
|
onChanged: (newText) => _onChangedDebounced([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,
|
||||||
|
maxLengthEnforced: widget.maxLengthEnforced,
|
||||||
|
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() {
|
||||||
|
_onChangedDebounced.cancel();
|
||||||
|
_controller?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
/// Creates a controller for an editable text field from an initial [text].
|
||||||
|
factory MessageInputController.fromText(String? text) =>
|
||||||
|
MessageInputController._(Message(text: text));
|
||||||
|
|
||||||
|
/// Creates a controller for an editable text field from an initial [attachments].
|
||||||
|
factory MessageInputController.fromAttachments(
|
||||||
|
List<Attachment> attachments,
|
||||||
|
) =>
|
||||||
|
MessageInputController._(Message(attachments: attachments));
|
||||||
|
|
||||||
|
MessageInputController._(Message message)
|
||||||
|
: _textEditingController = TextEditingController(text: message.text),
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
///
|
||||||
|
TextEditingController get textEditingController => _textEditingController;
|
||||||
|
final TextEditingController _textEditingController;
|
||||||
|
|
||||||
|
///
|
||||||
|
String get text => value.text ?? '';
|
||||||
|
|
||||||
|
///
|
||||||
|
set message(Message message) {
|
||||||
|
value = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
set text(String? newText) {
|
||||||
|
value = value.copyWith(text: newText);
|
||||||
|
_textEditingController.text = newText ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
set textEditingValue(TextEditingValue newValue) {
|
||||||
|
_textEditingController.value = newValue;
|
||||||
|
value = value.copyWith(text: _textEditingController.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
List<Attachment> get attachments => value.attachments;
|
||||||
|
|
||||||
|
set attachments(List<Attachment> attachments) {
|
||||||
|
value = value.copyWith(attachments: attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
get baseOffset {
|
||||||
|
return textEditingController.selection.baseOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void addAttachment(Attachment attachment) {
|
||||||
|
attachments = [...attachments, attachment];
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void addAttachmentAt(int index, Attachment attachment) {
|
||||||
|
attachments = [...attachments]..insert(index, attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void removeAttachment(Attachment attachment) {
|
||||||
|
attachments = [...attachments]..remove(attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void removeAttachmentById(String attachmentId) {
|
||||||
|
attachments = [...attachments]..removeWhere((it) => it.id == attachmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void removeAttachmentAt(int index) {
|
||||||
|
attachments = [...attachments]..removeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
List<User> get mentionedUsers => value.mentionedUsers;
|
||||||
|
|
||||||
|
set mentionedUsers(List<User> users) {
|
||||||
|
value = value.copyWith(mentionedUsers: users);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void addMentionedUser(User user) {
|
||||||
|
mentionedUsers = [...mentionedUsers, user];
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void removeMentionedUser(User user) {
|
||||||
|
mentionedUsers = [...mentionedUsers]..remove(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void removeMentionedUserById(String userId) {
|
||||||
|
mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the [value] to empty.
|
||||||
|
///
|
||||||
|
/// After calling this function, [text], [attachments] and [mentionedUsers]
|
||||||
|
/// all will be empty.
|
||||||
|
///
|
||||||
|
/// Calling this will notify all the listeners of this [MessageInputController]
|
||||||
|
/// that they need to update (it 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_textEditingController.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<MessageInputController> {
|
||||||
|
/// Creates a [RestorableMessageInputController].
|
||||||
|
///
|
||||||
|
/// This constructor treats a null `text` argument as if it were the empty
|
||||||
|
/// string.
|
||||||
|
RestorableMessageInputController({Message? message})
|
||||||
|
: _initialValue = message ?? Message();
|
||||||
|
|
||||||
|
/// Creates a [RestorableMessageInputController] from an initial
|
||||||
|
/// [TextEditingValue].
|
||||||
|
///
|
||||||
|
/// This constructor treats a null `value` argument as if it were
|
||||||
|
/// [TextEditingValue.empty].
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export 'src/better_stream_builder.dart';
|
|||||||
export 'src/channel_list_core.dart' hide ChannelListCoreState;
|
export 'src/channel_list_core.dart' hide ChannelListCoreState;
|
||||||
export 'src/channels_bloc.dart';
|
export 'src/channels_bloc.dart';
|
||||||
export 'src/lazy_load_scroll_view.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_list_core.dart' hide MessageListCoreState;
|
||||||
export 'src/message_search_bloc.dart';
|
export 'src/message_search_bloc.dart';
|
||||||
export 'src/message_search_list_core.dart' hide MessageSearchListCoreState;
|
export 'src/message_search_list_core.dart' hide MessageSearchListCoreState;
|
||||||
|
|||||||
Reference in New Issue
Block a user