added basic replacements for controller

This commit is contained in:
Deven Joshi
2021-10-19 15:31:20 +05:30
parent 4e6fd97b55
commit 011fb1b2d9
5 changed files with 1125 additions and 141 deletions
@@ -178,7 +178,7 @@ class MessageInput extends StatefulWidget {
this.keyboardType = TextInputType.multiline,
this.disableAttachments = false,
this.initialMessage,
this.textEditingController,
this.messageInputController,
this.actions = const [],
this.actionsLocation = ActionsLocation.left,
this.attachmentThumbnailBuilders,
@@ -256,7 +256,7 @@ class MessageInput extends StatefulWidget {
final bool hideSendAsDm;
/// The text controller of the TextField
final TextEditingController? textEditingController;
final MessageInputController? messageInputController;
/// List of action widgets
final List<Widget> actions;
@@ -339,9 +339,6 @@ class MessageInput extends StatefulWidget {
/// State of [MessageInput]
class MessageInputState extends State<MessageInput> {
final _attachments = <String, Attachment>{};
final List<User> _mentionedUsers = [];
final _imagePicker = ImagePicker();
late final _focusNode = widget.focusNode ?? FocusNode();
bool _inputEnabled = true;
@@ -356,15 +353,15 @@ class MessageInputState extends State<MessageInput> {
int _filePickerIndex = 0;
/// The editing controller passed to the input TextField
late final TextEditingController textEditingController =
widget.textEditingController ?? TextEditingController();
late final MessageInputController messageInputController =
widget.messageInputController ?? MessageInputController();
late StreamChatThemeData _streamChatTheme;
late MessageInputThemeData _messageInputTheme;
bool get _hasQuotedMessage => widget.quotedMessage != null;
bool get _messageIsPresent => textEditingController.text.trim().isNotEmpty;
bool get _messageIsPresent => messageInputController.text.trim().isNotEmpty;
@override
void initState() {
@@ -372,7 +369,8 @@ class MessageInputState extends State<MessageInput> {
if (widget.editMessage != null || widget.initialMessage != null) {
_parseExistingMessage(widget.editMessage ?? widget.initialMessage!);
}
textEditingController.addListener(_onChangedDebounced);
messageInputController.textEditingController
.addListener(_onChangedDebounced);
_focusNode.addListener(_focusNodeListener);
}
@@ -414,64 +412,67 @@ class MessageInputState extends State<MessageInput> {
@override
Widget build(BuildContext context) {
Widget child = DecoratedBox(
decoration: BoxDecoration(
color: _messageInputTheme.inputBackgroundColor,
),
child: SafeArea(
child: GestureDetector(
onPanUpdate: (details) {
if (details.delta.dy > 0) {
_focusNode.unfocus();
if (_openFilePickerSection) {
setState(() {
_openFilePickerSection = false;
});
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;
});
}
}
}
},
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 (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(
visible: _focusNode.hasFocus &&
textEditingController.text.isNotEmpty &&
textEditingController.selection.baseOffset > 0 &&
textEditingController.text
messageInputController.text.isNotEmpty &&
messageInputController.baseOffset > 0 &&
messageInputController.text
.substring(
0,
textEditingController.selection.baseOffset,
messageInputController.baseOffset,
)
.contains(':'),
widget: _buildEmojiOverlay(),
@@ -591,7 +592,8 @@ class MessageInputState extends State<MessageInput> {
late Widget sendButton;
if (_timeOut > 0) {
sendButton = _CountdownButton(count: _timeOut);
} else if (!_messageIsPresent && _attachments.isEmpty) {
} else if (!_messageIsPresent &&
messageInputController.attachments.isEmpty) {
sendButton = widget.idleSendButton ?? _buildIdleSendButton(context);
} else {
sendButton = widget.activeSendButton != null
@@ -700,7 +702,7 @@ class MessageInputState extends State<MessageInput> {
maxLines: null,
onSubmitted: (_) => sendMessage(),
keyboardType: widget.keyboardType,
controller: textEditingController,
controller: messageInputController.textEditingController,
focusNode: _focusNode,
style: _messageInputTheme.inputTextStyle,
autofocus: widget.autofocus,
@@ -823,7 +825,7 @@ class MessageInputState extends State<MessageInput> {
late final _onChangedDebounced = debounce(
() {
var value = textEditingController.text;
var value = messageInputController.text;
if (!mounted) return;
value = value.trim();
@@ -853,7 +855,7 @@ class MessageInputState extends State<MessageInput> {
if (_commandEnabled && _chosenCommand!.name == 'giphy') {
return context.translations.searchGifLabel;
}
if (_attachments.isNotEmpty) {
if (messageInputController.attachments.isNotEmpty) {
return context.translations.addACommentOrSendLabel;
}
if (_timeOut != 0) {
@@ -865,12 +867,17 @@ class MessageInputState extends State<MessageInput> {
void _checkEmoji(String s, BuildContext context) {
if (s.isNotEmpty &&
textEditingController.selection.baseOffset > 0 &&
textEditingController.text
.substring(0, textEditingController.selection.baseOffset)
messageInputController.baseOffset > 0 &&
messageInputController.text
.substring(
0,
messageInputController.baseOffset,
)
.contains(':')) {
final textToSelection = textEditingController.text
.substring(0, textEditingController.value.selection.start);
final textToSelection = messageInputController.text.substring(
0,
messageInputController.textEditingController.value.selection.start,
);
final splits = textToSelection.split(':');
final query = splits[splits.length - 2].toLowerCase();
final emoji = Emoji.byName(query);
@@ -883,9 +890,12 @@ class MessageInputState extends State<MessageInput> {
void _checkMentions(String s, BuildContext context) {
if (s.isNotEmpty &&
textEditingController.selection.baseOffset > 0 &&
textEditingController.text
.substring(0, textEditingController.selection.baseOffset)
messageInputController.baseOffset > 0 &&
messageInputController.text
.substring(
0,
messageInputController.baseOffset,
)
.split(' ')
.last
.contains('@')) {
@@ -921,7 +931,7 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildCommandsOverlayEntry() {
final text = textEditingController.text.trimLeft();
final text = messageInputController.text.trimLeft();
final renderObject = context.findRenderObject() as RenderBox?;
if (renderObject == null) {
@@ -937,16 +947,16 @@ class MessageInputState extends State<MessageInput> {
Widget _buildFilePickerSection() {
final _attachmentContainsFile =
_attachments.values.any((it) => it.type == 'file');
messageInputController.attachments.any((it) => it.type == 'file');
final attachmentLimitCrossed =
_attachments.length >= widget.attachmentLimit;
messageInputController.attachments.length >= widget.attachmentLimit;
Color _getIconColor(int index) {
final streamChatThemeData = _streamChatTheme;
switch (index) {
case 0:
return _attachments.isEmpty
return messageInputController.attachments.isEmpty
? streamChatThemeData.colorTheme.accentPrimary
: (!_attachmentContainsFile
? streamChatThemeData.colorTheme.accentPrimary
@@ -955,7 +965,7 @@ class MessageInputState extends State<MessageInput> {
case 1:
return _attachmentContainsFile
? streamChatThemeData.colorTheme.accentPrimary
: (_attachments.isEmpty
: (messageInputController.attachments.isEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5)
: streamChatThemeData.colorTheme.textHighEmphasis
@@ -963,7 +973,8 @@ class MessageInputState extends State<MessageInput> {
case 2:
return attachmentLimitCrossed
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: _attachmentContainsFile && _attachments.isNotEmpty
: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
@@ -971,7 +982,8 @@ class MessageInputState extends State<MessageInput> {
case 3:
return attachmentLimitCrossed
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: _attachmentContainsFile && _attachments.isNotEmpty
: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
@@ -1001,26 +1013,26 @@ class MessageInputState extends State<MessageInput> {
icon: StreamSvgIcon.pictures(
color: _getIconColor(0),
),
onPressed:
_attachmentContainsFile && _attachments.isNotEmpty
? null
: () {
setState(() {
_filePickerIndex = 0;
});
},
onPressed: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? null
: () {
setState(() {
_filePickerIndex = 0;
});
},
),
IconButton(
iconSize: 32,
icon: StreamSvgIcon.files(
color: _getIconColor(1),
),
onPressed:
!_attachmentContainsFile && _attachments.isNotEmpty
? null
: () {
pickFile(DefaultAttachmentTypes.file);
},
onPressed: !_attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? null
: () {
pickFile(DefaultAttachmentTypes.file);
},
),
IconButton(
icon: StreamSvgIcon.camera(
@@ -1028,7 +1040,7 @@ class MessageInputState extends State<MessageInput> {
),
onPressed: attachmentLimitCrossed ||
(_attachmentContainsFile &&
_attachments.isNotEmpty)
messageInputController.attachments.isNotEmpty)
? null
: () {
pickFile(
@@ -1044,7 +1056,7 @@ class MessageInputState extends State<MessageInput> {
),
onPressed: attachmentLimitCrossed ||
(_attachmentContainsFile &&
_attachments.isNotEmpty)
messageInputController.attachments.isNotEmpty)
? null
: () {
pickFile(
@@ -1088,11 +1100,15 @@ class MessageInputState extends State<MessageInput> {
filePickerIndex: _filePickerIndex,
streamChatTheme: _streamChatTheme,
containsFile: _attachmentContainsFile,
selectedMedias: _attachments.keys.toList(),
selectedMedias: messageInputController.attachments
.map((e) => e.id)
.toList(),
onAddMoreFilesClick: pickFile,
onMediaSelected: (media) {
if (_attachments.containsKey(media.id)) {
setState(() => _attachments.remove(media.id));
if (messageInputController.attachments
.any((e) => e.id == media.id)) {
setState(() => messageInputController.attachments
.removeWhere((e) => e.id == media.id));
} else {
_addAssetAttachment(media);
}
@@ -1163,12 +1179,16 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildMentionsOverlayEntry() {
if (textEditingController.value.selection.start < 0) {
if (messageInputController.textEditingController.value.selection.start <
0) {
return const Offstage();
}
final splits = textEditingController.text
.substring(0, textEditingController.value.selection.start)
final splits = messageInputController.text
.substring(
0,
messageInputController.textEditingController.value.selection.start,
)
.split('@');
final query = splits.last.toLowerCase();
@@ -1196,14 +1216,15 @@ class MessageInputState extends State<MessageInput> {
size: Size(renderObject.size.width - 16, 400),
mentionsTileBuilder: tileBuilder,
onMentionUserTap: (user) {
_mentionedUsers.add(user);
messageInputController.mentionedUsers.add(user);
splits[splits.length - 1] = user.name;
final rejoin = splits.join('@');
textEditingController.value = TextEditingValue(
messageInputController.textEditingController.value = TextEditingValue(
text: rejoin +
textEditingController.text.substring(
textEditingController.selection.start,
messageInputController.text.substring(
messageInputController
.textEditingController.value.selection.start,
),
selection: TextSelection.collapsed(
offset: rejoin.length,
@@ -1216,12 +1237,15 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildEmojiOverlay() {
if (textEditingController.value.selection.baseOffset < 0) {
if (messageInputController.baseOffset < 0) {
return const Offstage();
}
final splits = textEditingController.text
.substring(0, textEditingController.value.selection.baseOffset)
final splits = messageInputController.text
.substring(
0,
messageInputController.baseOffset,
)
.split(':');
final query = splits.last.toLowerCase();
@@ -1240,10 +1264,11 @@ class MessageInputState extends State<MessageInput> {
void _chooseEmoji(List<String> splits, Emoji emoji) {
final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!;
textEditingController.value = TextEditingValue(
messageInputController.textEditingController.value = TextEditingValue(
text: rejoin +
textEditingController.text
.substring(textEditingController.selection.start),
messageInputController.text.substring(
messageInputController.textEditingController.selection.start,
),
selection: TextSelection.collapsed(
offset: rejoin.length,
),
@@ -1251,7 +1276,7 @@ class MessageInputState extends State<MessageInput> {
}
void _setCommand(Command c) {
textEditingController.clear();
messageInputController.clear();
setState(() {
_chosenCommand = c;
_commandEnabled = true;
@@ -1273,11 +1298,11 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildAttachments() {
if (_attachments.isEmpty) return const Offstage();
final fileAttachments = _attachments.values
if (messageInputController.attachments.isEmpty) return const Offstage();
final fileAttachments = messageInputController.attachments
.where((it) => it.type == 'file')
.toList(growable: false);
final remainingAttachments = _attachments.values
final remainingAttachments = messageInputController.attachments
.where((it) => it.type != 'file')
.toList(growable: false);
return Column(
@@ -1364,7 +1389,10 @@ class MessageInputState extends State<MessageInput> {
focusElevation: 0,
hoverElevation: 0,
onPressed: () {
setState(() => _attachments.remove(attachment.id));
setState(
() => messageInputController.attachments
.removeWhere((e) => e.id == attachment.id),
);
},
fillColor:
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
@@ -1443,7 +1471,7 @@ class MessageInputState extends State<MessageInput> {
}
Widget _buildCommandButton(BuildContext context) {
final s = textEditingController.text.trim();
final s = messageInputController.text.trim();
final defaultButton = IconButton(
icon: StreamSvgIcon.lightning(
color: s.isNotEmpty
@@ -1574,10 +1602,11 @@ class MessageInputState extends State<MessageInput> {
setState(() => _addAttachments([attachment]));
}
/// Adds an attachment to the [_attachments] map
/// Adds an attachment to the [messageInputController.attachments] map
void _addAttachments(Iterable<Attachment> attachments) {
final limit = widget.attachmentLimit;
final length = _attachments.length + attachments.length;
final length =
messageInputController.attachments.length + attachments.length;
if (length > limit) {
final onAttachmentLimitExceed = widget.onAttachmentLimitExceed;
if (onAttachmentLimitExceed != null) {
@@ -1591,7 +1620,7 @@ class MessageInputState extends State<MessageInput> {
);
}
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
Future<void> sendMessage() async {
var text = textEditingController.text.trim();
if (text.isEmpty && _attachments.isEmpty) {
var text = messageInputController.text.trim();
if (text.isEmpty && messageInputController.attachments.isEmpty) {
return;
}
@@ -1765,10 +1794,8 @@ class MessageInputState extends State<MessageInput> {
text = '${'/${_chosenCommand!.name} '}$text';
}
final attachments = [..._attachments.values];
textEditingController.clear();
_attachments.clear();
messageInputController.clear();
messageInputController.attachments.clear();
widget.onQuotedMessageCleared?.call();
setState(() {
@@ -1779,17 +1806,19 @@ class MessageInputState extends State<MessageInput> {
if (widget.editMessage != null) {
message = widget.editMessage!.copyWith(
text: text,
attachments: attachments,
mentionedUsers:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
attachments: messageInputController.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:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
attachments: messageInputController.attachments,
mentionedUsers: messageInputController.mentionedUsers
.where((u) => text.contains('@${u.name}'))
.toList(),
showInChannel: widget.parentMessage != null ? _sendAsDm : null,
);
}
@@ -1810,7 +1839,7 @@ class MessageInputState extends State<MessageInput> {
await streamChannel.reloadChannel();
}
_mentionedUsers.clear();
messageInputController.mentionedUsers.clear();
try {
Future sendingFuture;
@@ -1909,13 +1938,15 @@ class MessageInputState extends State<MessageInput> {
void _parseExistingMessage(Message message) {
final messageText = message.text;
if (messageText != null) textEditingController.text = messageText;
if (messageText != null) messageInputController.text = messageText;
_addAttachments(message.attachments);
}
@override
void dispose() {
textEditingController.dispose();
messageInputController.textEditingController
.removeListener(_onChangedDebounced);
messageInputController.dispose();
_focusNode.removeListener(_focusNodeListener);
_stopSlowMode();
_onChangedDebounced.cancel();