From 011fb1b2d9831f4add1d1b1ae8db2003b9c1573b Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 19 Oct 2021 15:31:20 +0530 Subject: [PATCH 01/58] added basic replacements for controller --- .../lib/src/message_input.dart | 313 +++---- .../src/mip/stream_message_send_button.dart | 0 .../src/mip/stream_message_text_field.dart | 782 ++++++++++++++++++ .../lib/src/message_input_controller.dart | 170 ++++ .../lib/stream_chat_flutter_core.dart | 1 + 5 files changed, 1125 insertions(+), 141 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart create mode 100644 packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart create mode 100644 packages/stream_chat_flutter_core/lib/src/message_input_controller.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 08fc2a7b..3f8e22d1 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -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 actions; @@ -339,9 +339,6 @@ class MessageInput extends StatefulWidget { /// State of [MessageInput] class MessageInputState extends State { - final _attachments = {}; - final List _mentionedUsers = []; - final _imagePicker = ImagePicker(); late final _focusNode = widget.focusNode ?? FocusNode(); bool _inputEnabled = true; @@ -356,15 +353,15 @@ class MessageInputState extends State { 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 { 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 { @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( + 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 { ), 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 { 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 { 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 { 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 { 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 { 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 { 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 { } 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 { 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 { 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 { 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 { 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 { 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 { ), onPressed: attachmentLimitCrossed || (_attachmentContainsFile && - _attachments.isNotEmpty) + messageInputController.attachments.isNotEmpty) ? null : () { pickFile( @@ -1044,7 +1056,7 @@ class MessageInputState extends State { ), onPressed: attachmentLimitCrossed || (_attachmentContainsFile && - _attachments.isNotEmpty) + messageInputController.attachments.isNotEmpty) ? null : () { pickFile( @@ -1088,11 +1100,15 @@ class MessageInputState extends State { 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 { } 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 { 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 { } 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 { void _chooseEmoji(List 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 { } void _setCommand(Command c) { - textEditingController.clear(); + messageInputController.clear(); setState(() { _chosenCommand = c; _commandEnabled = true; @@ -1273,11 +1298,11 @@ class MessageInputState extends State { } 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 { 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 { } 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 { setState(() => _addAttachments([attachment])); } - /// Adds an attachment to the [_attachments] map + /// Adds an attachment to the [messageInputController.attachments] map void _addAttachments(Iterable 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 { ); } for (final attachment in attachments) { - _attachments[attachment.id] = attachment; + messageInputController.addAttachment(attachment); } } @@ -1754,8 +1783,8 @@ class MessageInputState extends State { /// Sends the current message Future 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 { 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 { 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 { await streamChannel.reloadChannel(); } - _mentionedUsers.clear(); + messageInputController.mentionedUsers.clear(); try { Future sendingFuture; @@ -1909,13 +1938,15 @@ class MessageInputState extends State { 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(); diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart new file mode 100644 index 00000000..e69de29b diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart new file mode 100644 index 00000000..48ece391 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart @@ -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? 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? onSubmitted; + + /// {@macro flutter.widgets.editableText.onAppPrivateCommand} + final AppPrivateCommandCallback? onAppPrivateCommand; + + /// {@macro flutter.widgets.editableText.inputFormatters} + final List? 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], + /// [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? 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, + defaultValue: null)); + properties + .add(DiagnosticsProperty('enabled', enabled, defaultValue: null)); + properties.add(DiagnosticsProperty( + 'decoration', decoration, + defaultValue: const InputDecoration())); + properties.add(DiagnosticsProperty( + 'keyboardType', keyboardType, + defaultValue: TextInputType.text)); + properties.add( + DiagnosticsProperty('style', style, defaultValue: null)); + properties.add( + DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); + properties.add(DiagnosticsProperty( + 'obscuringCharacter', obscuringCharacter, + defaultValue: '•')); + properties.add(DiagnosticsProperty('obscureText', obscureText, + defaultValue: false)); + properties.add(DiagnosticsProperty('autocorrect', autocorrect, + defaultValue: true)); + properties.add(EnumProperty( + 'smartDashesType', smartDashesType, + defaultValue: + obscureText ? SmartDashesType.disabled : SmartDashesType.enabled)); + properties.add(EnumProperty( + 'smartQuotesType', smartQuotesType, + defaultValue: + obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled)); + properties.add(DiagnosticsProperty( + 'enableSuggestions', enableSuggestions, + defaultValue: true)); + properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); + properties.add(IntProperty('minLines', minLines, defaultValue: null)); + properties.add( + DiagnosticsProperty('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, + defaultValue: null)); + properties.add(EnumProperty( + 'textInputAction', textInputAction, + defaultValue: null)); + properties.add(EnumProperty( + 'textCapitalization', textCapitalization, + defaultValue: TextCapitalization.none)); + properties.add(EnumProperty('textAlign', textAlign, + defaultValue: TextAlign.start)); + properties.add(DiagnosticsProperty( + 'textAlignVertical', textAlignVertical, + defaultValue: null)); + properties.add(EnumProperty('textDirection', textDirection, + defaultValue: null)); + properties + .add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); + properties + .add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); + properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, + defaultValue: null)); + properties + .add(ColorProperty('cursorColor', cursorColor, defaultValue: null)); + properties.add(DiagnosticsProperty( + 'keyboardAppearance', keyboardAppearance, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollPadding', scrollPadding, + defaultValue: const EdgeInsets.all(20.0))); + properties.add(FlagProperty('selectionEnabled', + value: selectionEnabled, + defaultValue: true, + ifFalse: 'selection disabled')); + properties.add(DiagnosticsProperty( + 'selectionControls', selectionControls, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollController', scrollController, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollPhysics', scrollPhysics, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, + defaultValue: true)); + } +} + +class _StreamMessageTextFieldState extends State + with RestorationMixin { + 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(); + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart new file mode 100644 index 00000000..365dc017 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -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 { + /// 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 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 get attachments => value.attachments; + + set attachments(List 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 get mentionedUsers => value.mentionedUsers; + + set mentionedUsers(List 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 { + /// 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); +} diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart index 8bd41c76..bf191ace 100644 --- a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -7,6 +7,7 @@ export 'src/better_stream_builder.dart'; export 'src/channel_list_core.dart' hide ChannelListCoreState; export 'src/channels_bloc.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_search_bloc.dart'; export 'src/message_search_list_core.dart' hide MessageSearchListCoreState; From 4459253f49a2c514b1bc9143834ace881d468f8d Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 19 Oct 2021 16:08:25 +0530 Subject: [PATCH 02/58] sendAsDm changes --- .../lib/src/message_input.dart | 22 +++++++------- .../lib/src/message_input_controller.dart | 30 +++++++++++++++---- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 3f8e22d1..982d5cc5 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -348,7 +348,6 @@ class MessageInputState extends State { Command? _chosenCommand; bool _actionsShrunk = false; - bool _sendAsDm = false; bool _openFilePickerSection = false; int _filePickerIndex = 0; @@ -535,7 +534,7 @@ class MessageInputState extends State { height: 16, width: 16, foregroundDecoration: BoxDecoration( - border: _sendAsDm + border: messageInputController.showInChannel ? null : Border.all( color: _streamChatTheme.colorTheme.textHighEmphasis @@ -547,19 +546,20 @@ class MessageInputState extends State { child: Center( child: Material( borderRadius: BorderRadius.circular(3), - color: _sendAsDm + color: messageInputController.showInChannel ? _streamChatTheme.colorTheme.accentPrimary : _streamChatTheme.colorTheme.barsBg, child: InkWell( onTap: () { setState(() { - _sendAsDm = !_sendAsDm; + messageInputController.showInChannel = + !messageInputController.showInChannel; }); }, child: AnimatedCrossFade( duration: const Duration(milliseconds: 300), reverseDuration: const Duration(milliseconds: 300), - crossFadeState: _sendAsDm + crossFadeState: messageInputController.showInChannel ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: StreamSvgIcon.check( @@ -1216,7 +1216,7 @@ class MessageInputState extends State { size: Size(renderObject.size.width - 16, 400), mentionsTileBuilder: tileBuilder, onMentionUserTap: (user) { - messageInputController.mentionedUsers.add(user); + messageInputController.addMentionedUser(user); splits[splits.length - 1] = user.name; final rejoin = splits.join('@'); @@ -1794,8 +1794,8 @@ class MessageInputState extends State { text = '${'/${_chosenCommand!.name} '}$text'; } - messageInputController.clear(); - messageInputController.attachments.clear(); + messageInputController.text = ''; + messageInputController.clearAttachments(); widget.onQuotedMessageCleared?.call(); setState(() { @@ -1819,7 +1819,9 @@ class MessageInputState extends State { mentionedUsers: messageInputController.mentionedUsers .where((u) => text.contains('@${u.name}')) .toList(), - showInChannel: widget.parentMessage != null ? _sendAsDm : null, + showInChannel: widget.parentMessage != null + ? messageInputController.showInChannel + : null, ); } @@ -1839,7 +1841,7 @@ class MessageInputState extends State { await streamChannel.reloadChannel(); } - messageInputController.mentionedUsers.clear(); + messageInputController.clearMentionedUsers(); try { Future sendingFuture; diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 365dc017..96026530 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -44,11 +44,26 @@ class MessageInputController extends ValueNotifier { _textEditingController.text = newText ?? ''; } + /// set textEditingValue(TextEditingValue newValue) { _textEditingController.value = newValue; value = value.copyWith(text: _textEditingController.text); } + /// + get baseOffset { + return textEditingController.selection.baseOffset; + } + + set showInChannel(bool newValue) { + value = value.copyWith(showInChannel: newValue); + } + + /// + bool get showInChannel { + return value.showInChannel ?? false; + } + /// List get attachments => value.attachments; @@ -56,11 +71,6 @@ class MessageInputController extends ValueNotifier { value = value.copyWith(attachments: attachments); } - /// - get baseOffset { - return textEditingController.selection.baseOffset; - } - /// void addAttachment(Attachment attachment) { attachments = [...attachments, attachment]; @@ -86,6 +96,11 @@ class MessageInputController extends ValueNotifier { attachments = [...attachments]..removeAt(index); } + /// + void clearAttachments() { + attachments = []; + } + /// List get mentionedUsers => value.mentionedUsers; @@ -108,6 +123,11 @@ class MessageInputController extends ValueNotifier { mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId); } + /// + void clearMentionedUsers() { + mentionedUsers = []; + } + /// Set the [value] to empty. /// /// After calling this function, [text], [attachments] and [mentionedUsers] From 8e7e6c9542f28b24780e6211e7f1bf90fbb80222 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 22 Oct 2021 18:51:06 +0530 Subject: [PATCH 03/58] fixed texteditingcontroller issues --- .../lib/src/message_input.dart | 21 +++++++++---------- .../src/mip/stream_message_send_button.dart | 1 + .../src/mip/stream_message_text_field.dart | 11 +++------- .../lib/stream_chat_flutter.dart | 1 + .../lib/src/message_input_controller.dart | 15 ++++++++++--- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 982d5cc5..e1ab4b90 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -696,13 +696,13 @@ class MessageInputState extends State { _buildAttachments(), LimitedBox( maxHeight: widget.maxHeight, - child: TextField( + child: StreamMessageTextField( key: const Key('messageInputText'), enabled: _inputEnabled, maxLines: null, onSubmitted: (_) => sendMessage(), keyboardType: widget.keyboardType, - controller: messageInputController.textEditingController, + controller: messageInputController, focusNode: _focusNode, style: _messageInputTheme.inputTextStyle, autofocus: widget.autofocus, @@ -876,7 +876,7 @@ class MessageInputState extends State { .contains(':')) { final textToSelection = messageInputController.text.substring( 0, - messageInputController.textEditingController.value.selection.start, + messageInputController.selectionStart, ); final splits = textToSelection.split(':'); final query = splits[splits.length - 2].toLowerCase(); @@ -1179,15 +1179,14 @@ class MessageInputState extends State { } Widget _buildMentionsOverlayEntry() { - if (messageInputController.textEditingController.value.selection.start < - 0) { + if (messageInputController.selectionStart < 0) { return const Offstage(); } final splits = messageInputController.text .substring( 0, - messageInputController.textEditingController.value.selection.start, + messageInputController.selectionStart, ) .split('@'); final query = splits.last.toLowerCase(); @@ -1223,8 +1222,7 @@ class MessageInputState extends State { messageInputController.textEditingController.value = TextEditingValue( text: rejoin + messageInputController.text.substring( - messageInputController - .textEditingController.value.selection.start, + messageInputController.selectionStart, ), selection: TextSelection.collapsed( offset: rejoin.length, @@ -1267,7 +1265,7 @@ class MessageInputState extends State { messageInputController.textEditingController.value = TextEditingValue( text: rejoin + messageInputController.text.substring( - messageInputController.textEditingController.selection.start, + messageInputController.selectionStart, ), selection: TextSelection.collapsed( offset: rejoin.length, @@ -1794,8 +1792,9 @@ class MessageInputState extends State { text = '${'/${_chosenCommand!.name} '}$text'; } - messageInputController.text = ''; - messageInputController.clearAttachments(); + messageInputController + ..text = '' + ..clearAttachments(); widget.onQuotedMessageCleared?.call(); setState(() { diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart index e69de29b..8b137891 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart +++ b/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart @@ -0,0 +1 @@ + diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart index 48ece391..43acce31 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart @@ -710,17 +710,13 @@ class _StreamMessageTextFieldState extends State 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]), + onChanged: (newText) { + _effectiveController.text = newText; + }, focusNode: widget.focusNode, decoration: widget.decoration, keyboardType: widget.keyboardType, @@ -775,7 +771,6 @@ class _StreamMessageTextFieldState extends State @override void dispose() { - _onChangedDebounced.cancel(); _controller?.dispose(); super.dispose(); } diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 0447fc68..948f0261 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -28,6 +28,7 @@ export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; +export 'src/mip/stream_message_text_field.dart'; export 'src/option_list_tile.dart'; export 'src/reaction_icon.dart'; export 'src/reaction_picker.dart'; diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 96026530..3f60efaf 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -32,16 +32,20 @@ class MessageInputController extends ValueNotifier { final TextEditingController _textEditingController; /// - String get text => value.text ?? ''; + String get text => _textEditingController.text; /// set message(Message message) { value = message; } - set text(String? newText) { + set text(String newText) { value = value.copyWith(text: newText); - _textEditingController.text = newText ?? ''; + _textEditingController + ..text = newText + ..selection = TextSelection.fromPosition( + TextPosition(offset: _textEditingController.text.length), + ); } /// @@ -55,6 +59,11 @@ class MessageInputController extends ValueNotifier { return textEditingController.selection.baseOffset; } + /// + get selectionStart { + return textEditingController.selection.start; + } + set showInChannel(bool newValue) { value = value.copyWith(showInChannel: newValue); } From 79aa6aa979780415bbb1d32a91fa290f3809bb3e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 28 Oct 2021 18:25:32 +0530 Subject: [PATCH 04/58] fixes --- .../lib/src/message_input_controller.dart | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 3f60efaf..d49d964f 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -1,10 +1,10 @@ -import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat/stream_chat.dart'; +/// A Controller for storing and handling the [Message] class MessageInputController extends ValueNotifier { /// Creates a controller for an editable text field. /// @@ -17,7 +17,7 @@ class MessageInputController extends ValueNotifier { factory MessageInputController.fromText(String? text) => MessageInputController._(Message(text: text)); - /// Creates a controller for an editable text field from an initial [attachments]. + /// Creates a controller for an editable textfield from initial [attachments]. factory MessageInputController.fromAttachments( List attachments, ) => @@ -55,23 +55,17 @@ class MessageInputController extends ValueNotifier { } /// - get baseOffset { - return textEditingController.selection.baseOffset; - } + int get baseOffset => textEditingController.selection.baseOffset; /// - get selectionStart { - return textEditingController.selection.start; - } + int get selectionStart => textEditingController.selection.start; set showInChannel(bool newValue) { value = value.copyWith(showInChannel: newValue); } /// - bool get showInChannel { - return value.showInChannel ?? false; - } + bool get showInChannel => value.showInChannel ?? false; /// List get attachments => value.attachments; @@ -142,7 +136,7 @@ class MessageInputController extends ValueNotifier { /// After calling this function, [text], [attachments] and [mentionedUsers] /// all will be empty. /// - /// Calling this will notify all the listeners of this [MessageInputController] + /// Calling this will notify 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. From 82109e5e4cc7a544d802723bf696fada4c7a8045 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 2 Nov 2021 22:13:51 +0530 Subject: [PATCH 05/58] added stream message button, general refactor --- .../lib/src/message_input.dart | 120 ++++-------------- .../lib/src/mip/countdown_button.dart | 29 +++++ .../src/mip/stream_message_send_button.dart | 97 ++++++++++++++ .../lib/stream_chat_flutter.dart | 2 + .../lib/src/message_input_controller.dart | 22 ++-- 5 files changed, 164 insertions(+), 106 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/mip/countdown_button.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index e1ab4b90..596e61c4 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -79,6 +79,11 @@ typedef ActionButtonBuilder = Widget Function( IconButton defaultActionButton, ); +typedef AttachmentsPickerBuilder = Widget Function( + BuildContext context, + List attachments, +); + /// Location for actions on the [MessageInput] enum ActionsLocation { /// Align to left @@ -204,6 +209,7 @@ class MessageInput extends StatefulWidget { this.commandButtonBuilder, this.customOverlays = const [], this.mentionAllAppUsers = false, + this.attachmentsPickerBuilder, }) : assert( initialMessage == null || editMessage == null, "Can't provide both `initialMessage` and `editMessage`", @@ -322,6 +328,9 @@ class MessageInput extends StatefulWidget { /// Defaults to false. final bool mentionAllAppUsers; + /// Builds bottom sheet when attachment picker is opened. + final AttachmentsPickerBuilder? attachmentsPickerBuilder; + @override MessageInputState createState() => MessageInputState(); @@ -524,7 +533,7 @@ class MessageInputState extends State { widget.actionsLocation == ActionsLocation.right) _buildExpandActionsButton(context), if (widget.sendButtonLocation == SendButtonLocation.outside) - _animateSendButton(context), + _buildSendButton(context), ], ); @@ -588,27 +597,15 @@ class MessageInputState extends State { ], ); - Widget _animateSendButton(BuildContext context) { - late Widget sendButton; - if (_timeOut > 0) { - sendButton = _CountdownButton(count: _timeOut); - } else if (!_messageIsPresent && - messageInputController.attachments.isEmpty) { - sendButton = widget.idleSendButton ?? _buildIdleSendButton(context); - } else { - sendButton = widget.activeSendButton != null - ? InkWell( - onTap: sendMessage, - child: widget.activeSendButton, - ) - : _buildSendButton(context); - } - - return AnimatedSwitcher( - duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, - child: sendButton, - ); - } + Widget _buildSendButton(BuildContext context) => StreamMessageSendButton( + onSendMessage: sendMessage, + timeOut: _timeOut, + isIdle: + !_messageIsPresent && messageInputController.attachments.isEmpty, + isEditEnabled: widget.editMessage != null, + idleSendButton: widget.idleSendButton, + activeSendButton: widget.activeSendButton, + ); Widget _buildExpandActionsButton(BuildContext context) { final channel = StreamChannel.of(context).channel; @@ -817,7 +814,7 @@ class MessageInputState extends State { widget.actionsLocation == ActionsLocation.rightInside) _buildExpandActionsButton(context), if (widget.sendButtonLocation == SendButtonLocation.inside) - _animateSendButton(context), + _buildSendButton(context), ], ), ).merge(passedDecoration); @@ -993,6 +990,13 @@ class MessageInputState extends State { } } + if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) { + return widget.attachmentsPickerBuilder!( + context, + messageInputController.attachments, + ); + } + return AnimatedContainer( duration: _openFilePickerSection ? const Duration(milliseconds: 300) @@ -1736,49 +1740,6 @@ class MessageInputState extends State { }); } - Widget _buildIdleSendButton(BuildContext context) => Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon( - assetName: _getIdleSendIcon(), - color: _messageInputTheme.sendButtonIdleColor, - ), - ); - - Widget _buildSendButton(BuildContext context) => Padding( - padding: const EdgeInsets.all(8), - child: IconButton( - onPressed: sendMessage, - padding: const EdgeInsets.all(0), - splashRadius: 24, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - icon: StreamSvgIcon( - assetName: _getSendIcon(), - color: _messageInputTheme.sendButtonColor, - ), - ), - ); - - String _getIdleSendIcon() { - if (_commandEnabled) { - return 'Icon_search.svg'; - } else { - return 'Icon_circle_right.svg'; - } - } - - String _getSendIcon() { - if (widget.editMessage != null) { - return 'Icon_circle_up.svg'; - } else if (_commandEnabled) { - return 'Icon_search.svg'; - } else { - return 'Icon_circle_up.svg'; - } - } - /// Sends the current message Future sendMessage() async { var text = messageInputController.text.trim(); @@ -2080,30 +2041,3 @@ class _PickerWidgetState extends State<_PickerWidget> { ); } } - -class _CountdownButton extends StatelessWidget { - const _CountdownButton({ - Key? key, - required this.count, - }) : super(key: key); - - final int count; - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.all(8), - child: DecoratedBox( - decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.disabled, - shape: BoxShape.circle, - ), - child: SizedBox( - height: 24, - width: 24, - child: Center( - child: Text('$count'), - ), - ), - ), - ); -} diff --git a/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart b/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart new file mode 100644 index 00000000..e3d8bd32 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class CountdownButton extends StatelessWidget { + const CountdownButton({ + Key? key, + required this.count, + }) : super(key: key); + + final int count; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(8), + child: DecoratedBox( + decoration: BoxDecoration( + color: StreamChatTheme.of(context).colorTheme.disabled, + shape: BoxShape.circle, + ), + child: SizedBox( + height: 24, + width: 24, + child: Center( + child: Text('$count'), + ), + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart index 8b137891..5443fb18 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart +++ b/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart @@ -1 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +class StreamMessageSendButton extends StatelessWidget { + final int timeOut; + final bool isIdle; + final bool isCommandEnabled; + final bool isEditEnabled; + final Widget? idleSendButton; + final Widget? activeSendButton; + final VoidCallback onSendMessage; + + const StreamMessageSendButton({ + Key? key, + this.timeOut = 0, + this.isIdle = true, + this.isCommandEnabled = false, + this.isEditEnabled = false, + this.idleSendButton, + this.activeSendButton, + required this.onSendMessage, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var _streamChatTheme = StreamChatTheme.of(context); + + late Widget sendButton; + if (timeOut > 0) { + sendButton = CountdownButton(count: timeOut); + } else if (isIdle) { + sendButton = idleSendButton ?? _buildIdleSendButton(context); + } else { + sendButton = activeSendButton != null + ? InkWell( + onTap: onSendMessage, + child: activeSendButton, + ) + : _buildSendButton(context); + } + + return AnimatedSwitcher( + duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, + child: sendButton, + ); + } + + Widget _buildIdleSendButton(BuildContext context) { + var _messageInputTheme = MessageInputTheme.of(context); + + return Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon( + assetName: _getIdleSendIcon(), + color: _messageInputTheme.sendButtonIdleColor, + ), + ); + } + + Widget _buildSendButton(BuildContext context) { + var _messageInputTheme = MessageInputTheme.of(context); + + return Padding( + padding: const EdgeInsets.all(8), + child: IconButton( + onPressed: onSendMessage, + padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + icon: StreamSvgIcon( + assetName: _getSendIcon(), + color: _messageInputTheme.sendButtonColor, + ), + ), + ); + } + + String _getIdleSendIcon() { + if (isCommandEnabled) { + return 'Icon_search.svg'; + } else { + return 'Icon_circle_right.svg'; + } + } + + String _getSendIcon() { + if (isEditEnabled) { + return 'Icon_circle_up.svg'; + } else if (isCommandEnabled) { + return 'Icon_search.svg'; + } else { + return 'Icon_circle_up.svg'; + } + } +} diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 948f0261..f58b7439 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -28,6 +28,8 @@ export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; +export 'src/mip/countdown_button.dart'; +export 'src/mip/stream_message_send_button.dart'; export 'src/mip/stream_message_text_field.dart'; export 'src/option_list_tile.dart'; export 'src/reaction_icon.dart'; diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 3f60efaf..3339bc5b 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -1,10 +1,10 @@ -import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat/stream_chat.dart'; +/// Controller for storing and mutating a [Message] value. class MessageInputController extends ValueNotifier { /// Creates a controller for an editable text field. /// @@ -17,7 +17,8 @@ class MessageInputController extends ValueNotifier { factory MessageInputController.fromText(String? text) => MessageInputController._(Message(text: text)); - /// Creates a controller for an editable text field from an initial [attachments]. + /// Creates a controller for an editable text field from an initial + /// [attachments]. factory MessageInputController.fromAttachments( List attachments, ) => @@ -55,23 +56,17 @@ class MessageInputController extends ValueNotifier { } /// - get baseOffset { - return textEditingController.selection.baseOffset; - } + int get baseOffset => textEditingController.selection.baseOffset; /// - get selectionStart { - return textEditingController.selection.start; - } + int get selectionStart => textEditingController.selection.start; set showInChannel(bool newValue) { value = value.copyWith(showInChannel: newValue); } /// - bool get showInChannel { - return value.showInChannel ?? false; - } + bool get showInChannel => value.showInChannel ?? false; /// List get attachments => value.attachments; @@ -142,8 +137,9 @@ class MessageInputController extends ValueNotifier { /// 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, + /// 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() { From c6b04300d35e579f0a536233364ca1bebe38796e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 2 Nov 2021 22:27:11 +0530 Subject: [PATCH 06/58] added changelog --- packages/stream_chat_flutter/CHANGELOG.md | 8 ++++++++ packages/stream_chat_flutter_core/CHANGELOG.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 00f8412b..cc6cd374 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,11 @@ +## Upcoming + +✅ Added + +🛑️ Breaking Changes from `3.2.0` + +- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController` + ## 3.2.0 - Updated Dart SDK constraints to `>=2.14.0 <3.0.0` diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 77ac23a0..37354ebf 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +✅ Added + +- Added `MessageInputController` to hold `Message` related data. + ## 3.2.0 - Updated `stream_chat` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat/changelog). From 5c09bf21109ffc574ed96f573743345efcb661d4 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 3 Nov 2021 14:09:51 +0530 Subject: [PATCH 07/58] added new builder --- .../lib/src/message_input.dart | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 596e61c4..556a5ac1 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -79,9 +79,11 @@ typedef ActionButtonBuilder = Widget Function( IconButton defaultActionButton, ); -typedef AttachmentsPickerBuilder = Widget Function( +/// Widget builder for widgets that require may required data from the +/// [MessageInputController] +typedef MessageRelatedBuilder = Widget Function( BuildContext context, - List attachments, + MessageInputController messageInputController, ); /// Location for actions on the [MessageInput] @@ -210,6 +212,7 @@ class MessageInput extends StatefulWidget { this.customOverlays = const [], this.mentionAllAppUsers = false, this.attachmentsPickerBuilder, + this.sendButtonBuilder, }) : assert( initialMessage == null || editMessage == null, "Can't provide both `initialMessage` and `editMessage`", @@ -329,7 +332,10 @@ class MessageInput extends StatefulWidget { final bool mentionAllAppUsers; /// Builds bottom sheet when attachment picker is opened. - final AttachmentsPickerBuilder? attachmentsPickerBuilder; + final MessageRelatedBuilder? attachmentsPickerBuilder; + + /// Builder for creating send button + final MessageRelatedBuilder? sendButtonBuilder; @override MessageInputState createState() => MessageInputState(); @@ -597,15 +603,20 @@ class MessageInputState extends State { ], ); - Widget _buildSendButton(BuildContext context) => StreamMessageSendButton( - onSendMessage: sendMessage, - timeOut: _timeOut, - isIdle: - !_messageIsPresent && messageInputController.attachments.isEmpty, - isEditEnabled: widget.editMessage != null, - idleSendButton: widget.idleSendButton, - activeSendButton: widget.activeSendButton, - ); + Widget _buildSendButton(BuildContext context) { + if (widget.sendButtonBuilder != null) { + return widget.sendButtonBuilder!(context, messageInputController); + } + + return StreamMessageSendButton( + onSendMessage: sendMessage, + timeOut: _timeOut, + isIdle: !_messageIsPresent && messageInputController.attachments.isEmpty, + isEditEnabled: widget.editMessage != null, + idleSendButton: widget.idleSendButton, + activeSendButton: widget.activeSendButton, + ); + } Widget _buildExpandActionsButton(BuildContext context) { final channel = StreamChannel.of(context).channel; @@ -993,7 +1004,7 @@ class MessageInputState extends State { if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) { return widget.attachmentsPickerBuilder!( context, - messageInputController.attachments, + messageInputController, ); } From bf30102d2b4a676edfe8bab7436112a6b78f1df0 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 3 Nov 2021 14:11:56 +0530 Subject: [PATCH 08/58] analysis fixes --- .../stream_chat_flutter/lib/src/mip/countdown_button.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart b/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart index e3d8bd32..6e7b5d9b 100644 --- a/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart +++ b/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart @@ -1,12 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// Button for showing visual component of slow mode. class CountdownButton extends StatelessWidget { + + /// Constructor for creating [CountdownButton]. const CountdownButton({ Key? key, required this.count, }) : super(key: key); + /// Count of time remaining to show to the user. final int count; @override From 5a5063cddb31aa0b2c3ace5730b5ced8e843a221 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 3 Nov 2021 17:45:32 +0530 Subject: [PATCH 09/58] separated attachment picker --- .../lib/src/message_input.dart | 356 +-------------- .../lib/src/mip/countdown_button.dart | 1 - .../lib/src/mip/stream_attachment_picker.dart | 428 ++++++++++++++++++ .../lib/stream_chat_flutter.dart | 1 + 4 files changed, 445 insertions(+), 341 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 556a5ac1..67785cd2 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -364,7 +364,6 @@ class MessageInputState extends State { Command? _chosenCommand; bool _actionsShrunk = false; bool _openFilePickerSection = false; - int _filePickerIndex = 0; /// The editing controller passed to the input TextField late final MessageInputController messageInputController = @@ -954,53 +953,6 @@ class MessageInputState extends State { } Widget _buildFilePickerSection() { - final _attachmentContainsFile = - messageInputController.attachments.any((it) => it.type == 'file'); - - final attachmentLimitCrossed = - messageInputController.attachments.length >= widget.attachmentLimit; - - Color _getIconColor(int index) { - final streamChatThemeData = _streamChatTheme; - switch (index) { - case 0: - return messageInputController.attachments.isEmpty - ? streamChatThemeData.colorTheme.accentPrimary - : (!_attachmentContainsFile - ? streamChatThemeData.colorTheme.accentPrimary - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2)); - case 1: - return _attachmentContainsFile - ? streamChatThemeData.colorTheme.accentPrimary - : (messageInputController.attachments.isEmpty - ? streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2)); - case 2: - return attachmentLimitCrossed - ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : _attachmentContainsFile && - messageInputController.attachments.isNotEmpty - ? streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); - case 3: - return attachmentLimitCrossed - ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : _attachmentContainsFile && - messageInputController.attachments.isNotEmpty - ? streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); - default: - return Colors.black; - } - } - if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) { return widget.attachmentsPickerBuilder!( context, @@ -1008,191 +960,25 @@ class MessageInputState extends State { ); } - return AnimatedContainer( - duration: _openFilePickerSection - ? const Duration(milliseconds: 300) - : const Duration(), - curve: Curves.easeOut, - height: _openFilePickerSection ? _kMinMediaPickerSize : 0, - child: SingleChildScrollView( - child: SizedBox( - height: _kMinMediaPickerSize, - child: Material( - color: _streamChatTheme.colorTheme.inputBg, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - IconButton( - icon: StreamSvgIcon.pictures( - color: _getIconColor(0), - ), - onPressed: _attachmentContainsFile && - messageInputController.attachments.isNotEmpty - ? null - : () { - setState(() { - _filePickerIndex = 0; - }); - }, - ), - IconButton( - iconSize: 32, - icon: StreamSvgIcon.files( - color: _getIconColor(1), - ), - onPressed: !_attachmentContainsFile && - messageInputController.attachments.isNotEmpty - ? null - : () { - pickFile(DefaultAttachmentTypes.file); - }, - ), - IconButton( - icon: StreamSvgIcon.camera( - color: _getIconColor(2), - ), - onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && - messageInputController.attachments.isNotEmpty) - ? null - : () { - pickFile( - DefaultAttachmentTypes.image, - camera: true, - ); - }, - ), - IconButton( - padding: const EdgeInsets.all(0), - icon: StreamSvgIcon.record( - color: _getIconColor(3), - ), - onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && - messageInputController.attachments.isNotEmpty) - ? null - : () { - pickFile( - DefaultAttachmentTypes.video, - camera: true, - ); - }, - ), - ], - ), - DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.inputBg, - borderRadius: BorderRadius.circular(4), - ), - ), - ), - ), - ), - if (_openFilePickerSection) - Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - child: _PickerWidget( - filePickerIndex: _filePickerIndex, - streamChatTheme: _streamChatTheme, - containsFile: _attachmentContainsFile, - selectedMedias: messageInputController.attachments - .map((e) => e.id) - .toList(), - onAddMoreFilesClick: pickFile, - onMediaSelected: (media) { - if (messageInputController.attachments - .any((e) => e.id == media.id)) { - setState(() => messageInputController.attachments - .removeWhere((e) => e.id == media.id)); - } else { - _addAssetAttachment(media); - } - }, - ), - ), - ), - ], - ), - ), - ), - ), + return StreamAttachmentPicker( + messageInputController: messageInputController, + onFilePicked: pickFile, + isOpen: _openFilePickerSection, + pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0, + attachmentLimit: widget.attachmentLimit, + onAttachmentLimitExceeded: widget.onAttachmentLimitExceed, + maxAttachmentSize: widget.maxAttachmentSize, + compressedVideoQuality: widget.compressedVideoQuality, + compressedVideoFrameRate: widget.compressedVideoFrameRate, + onChangeInputState: (val) { + setState(() { + _inputEnabled = val; + }); + }, + onError: _showErrorAlert, ); } - void _addAssetAttachment(AssetEntity medium) async { - final mediaFile = await medium.originFile.timeout( - const Duration(seconds: 5), - onTimeout: () => medium.originFile, - ); - - if (mediaFile == null) return; - - var file = AttachmentFile( - path: mediaFile.path, - size: await mediaFile.length(), - bytes: mediaFile.readAsBytesSync(), - ); - - if (file.size! > widget.maxAttachmentSize) { - if (medium.type == AssetType.video && file.path != null) { - final mediaInfo = await (VideoService.compressVideo( - file.path!, - frameRate: widget.compressedVideoFrameRate, - quality: widget.compressedVideoQuality, - ) as FutureOr); - - if (mediaInfo.filesize! > widget.maxAttachmentSize) { - _showErrorAlert( - context.translations.fileTooLargeAfterCompressionError( - widget.maxAttachmentSize / (1024 * 1024), - ), - ); - return; - } - file = AttachmentFile( - name: file.name, - size: mediaInfo.filesize, - bytes: await mediaInfo.file?.readAsBytes(), - path: mediaInfo.path, - ); - } else { - _showErrorAlert(context.translations.fileTooLargeError( - widget.maxAttachmentSize / (1024 * 1024), - )); - return; - } - } - - setState(() { - final attachment = Attachment( - id: medium.id, - file: file, - type: medium.type == AssetType.image ? 'image' : 'video', - ); - _addAttachments([attachment]); - }); - } - Widget _buildMentionsOverlayEntry() { if (messageInputController.selectionStart < 0) { return const Offstage(); @@ -1942,113 +1728,3 @@ class MessageInputState extends State { super.didChangeDependencies(); } } - -class _PickerWidget extends StatefulWidget { - const _PickerWidget({ - Key? key, - required this.filePickerIndex, - required this.containsFile, - required this.selectedMedias, - required this.onAddMoreFilesClick, - required this.onMediaSelected, - required this.streamChatTheme, - }) : super(key: key); - - final int filePickerIndex; - final bool containsFile; - final List selectedMedias; - final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; - final void Function(AssetEntity) onMediaSelected; - final StreamChatThemeData streamChatTheme; - - @override - _PickerWidgetState createState() => _PickerWidgetState(); -} - -class _PickerWidgetState extends State<_PickerWidget> { - Future? requestPermission; - - @override - void initState() { - super.initState(); - requestPermission = PhotoManager.requestPermission(); - } - - @override - Widget build(BuildContext context) { - if (widget.filePickerIndex != 0) { - return const Offstage(); - } - return FutureBuilder( - future: requestPermission, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Offstage(); - } - - if (snapshot.data!) { - if (widget.containsFile) { - return GestureDetector( - onTap: () { - widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); - }, - child: Container( - constraints: const BoxConstraints.expand(), - color: widget.streamChatTheme.colorTheme.inputBg, - alignment: Alignment.center, - child: Text( - context.translations.addMoreFilesLabel, - style: TextStyle( - color: widget.streamChatTheme.colorTheme.accentPrimary, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } - return MediaListView( - selectedIds: widget.selectedMedias, - onSelect: widget.onMediaSelected, - ); - } - - return InkWell( - onTap: () async { - PhotoManager.openSetting(); - }, - child: Container( - color: widget.streamChatTheme.colorTheme.inputBg, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SvgPicture.asset( - 'svgs/icon_picture_empty_state.svg', - package: 'stream_chat_flutter', - height: 140, - color: widget.streamChatTheme.colorTheme.disabled, - ), - Text( - context.translations.enablePhotoAndVideoAccessMessage, - style: widget.streamChatTheme.textTheme.body.copyWith( - color: widget.streamChatTheme.colorTheme.textLowEmphasis, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - Center( - child: Text( - context.translations.allowGalleryAccessMessage, - style: widget.streamChatTheme.textTheme.bodyBold.copyWith( - color: widget.streamChatTheme.colorTheme.accentPrimary, - ), - ), - ), - ], - ), - ), - ); - }, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart b/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart index 6e7b5d9b..50f18e50 100644 --- a/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart +++ b/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart @@ -3,7 +3,6 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Button for showing visual component of slow mode. class CountdownButton extends StatelessWidget { - /// Constructor for creating [CountdownButton]. const CountdownButton({ Key? key, diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart new file mode 100644 index 00000000..904825fe --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart @@ -0,0 +1,428 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/media_list_view.dart'; +import 'package:stream_chat_flutter/src/video_service.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:video_compress/video_compress.dart'; + +typedef FilePickerCallback = void Function( + DefaultAttachmentTypes fileType, { + bool camera, +}); + +class StreamAttachmentPicker extends StatefulWidget { + final bool isOpen; + final double pickerSize; + final MessageInputController messageInputController; + final int attachmentLimit; + final AttachmentLimitExceedListener? onAttachmentLimitExceeded; + final ValueChanged? onChangeInputState; + final ValueChanged? onError; + final FilePickerCallback onFilePicked; + + /// Video quality to use when compressing the videos + final VideoQuality compressedVideoQuality; + + /// Frame rate to use when compressing the videos + final int compressedVideoFrameRate; + + /// Max attachment size in bytes + /// Defaults to 20 MB + /// do not set it if you're using our default CDN + final int maxAttachmentSize; + + const StreamAttachmentPicker({ + Key? key, + required this.messageInputController, + required this.onFilePicked, + this.isOpen = false, + this.pickerSize = 360.0, + this.attachmentLimit = 10, + this.onAttachmentLimitExceeded, + this.maxAttachmentSize = 20971520, + this.compressedVideoQuality = VideoQuality.DefaultQuality, + this.compressedVideoFrameRate = 30, + this.onChangeInputState, + this.onError, + }) : super(key: key); + + @override + State createState() => _StreamAttachmentPickerState(); +} + +class _StreamAttachmentPickerState extends State { + int _filePickerIndex = 0; + + @override + Widget build(BuildContext context) { + var _streamChatTheme = StreamChatTheme.of(context); + var messageInputController = widget.messageInputController; + + final _attachmentContainsFile = + messageInputController.attachments.any((it) => it.type == 'file'); + + final attachmentLimitCrossed = + messageInputController.attachments.length >= widget.attachmentLimit; + + Color _getIconColor(int index) { + final streamChatThemeData = _streamChatTheme; + switch (index) { + case 0: + return messageInputController.attachments.isEmpty + ? streamChatThemeData.colorTheme.accentPrimary + : (!_attachmentContainsFile + ? streamChatThemeData.colorTheme.accentPrimary + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2)); + case 1: + return _attachmentContainsFile + ? streamChatThemeData.colorTheme.accentPrimary + : (messageInputController.attachments.isEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2)); + case 2: + return attachmentLimitCrossed + ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) + : _attachmentContainsFile && + messageInputController.attachments.isNotEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); + case 3: + return attachmentLimitCrossed + ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) + : _attachmentContainsFile && + messageInputController.attachments.isNotEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); + default: + return Colors.black; + } + } + + return AnimatedContainer( + duration: + widget.isOpen ? const Duration(milliseconds: 300) : const Duration(), + curve: Curves.easeOut, + height: widget.isOpen ? widget.pickerSize : 0, + child: SingleChildScrollView( + child: SizedBox( + height: widget.pickerSize, + child: Material( + color: _streamChatTheme.colorTheme.inputBg, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + IconButton( + icon: StreamSvgIcon.pictures( + color: _getIconColor(0), + ), + onPressed: _attachmentContainsFile && + messageInputController.attachments.isNotEmpty + ? null + : () { + setState(() { + _filePickerIndex = 0; + }); + }, + ), + IconButton( + iconSize: 32, + icon: StreamSvgIcon.files( + color: _getIconColor(1), + ), + onPressed: !_attachmentContainsFile && + messageInputController.attachments.isNotEmpty + ? null + : () { + widget.onFilePicked(DefaultAttachmentTypes.file); + }, + ), + IconButton( + icon: StreamSvgIcon.camera( + color: _getIconColor(2), + ), + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && + messageInputController.attachments.isNotEmpty) + ? null + : () { + widget.onFilePicked( + DefaultAttachmentTypes.image, + camera: true, + ); + }, + ), + IconButton( + padding: const EdgeInsets.all(0), + icon: StreamSvgIcon.record( + color: _getIconColor(3), + ), + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && + messageInputController.attachments.isNotEmpty) + ? null + : () { + widget.onFilePicked( + DefaultAttachmentTypes.video, + camera: true, + ); + }, + ), + ], + ), + DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.inputBg, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + ), + if (widget.isOpen) + Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + child: _PickerWidget( + filePickerIndex: _filePickerIndex, + streamChatTheme: _streamChatTheme, + containsFile: _attachmentContainsFile, + selectedMedias: messageInputController.attachments + .map((e) => e.id) + .toList(), + onAddMoreFilesClick: widget.onFilePicked, + onMediaSelected: (media) { + if (messageInputController.attachments + .any((e) => e.id == media.id)) { + setState(() => messageInputController.attachments + .removeWhere((e) => e.id == media.id)); + } else { + _addAssetAttachment(media); + } + }, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _addAssetAttachment(AssetEntity medium) async { + final mediaFile = await medium.originFile.timeout( + const Duration(seconds: 5), + onTimeout: () => medium.originFile, + ); + + if (mediaFile == null) return; + + var file = AttachmentFile( + path: mediaFile.path, + size: await mediaFile.length(), + bytes: mediaFile.readAsBytesSync(), + ); + + if (file.size! > widget.maxAttachmentSize) { + if (medium.type == AssetType.video && file.path != null) { + final mediaInfo = await (VideoService.compressVideo( + file.path!, + frameRate: widget.compressedVideoFrameRate, + quality: widget.compressedVideoQuality, + ) as FutureOr); + + if (mediaInfo.filesize! > widget.maxAttachmentSize) { + widget.onError?.call( + context.translations.fileTooLargeAfterCompressionError( + widget.maxAttachmentSize / (1024 * 1024), + ), + ); + return; + } + file = AttachmentFile( + name: file.name, + size: mediaInfo.filesize, + bytes: await mediaInfo.file?.readAsBytes(), + path: mediaInfo.path, + ); + } else { + widget.onError?.call(context.translations.fileTooLargeError( + widget.maxAttachmentSize / (1024 * 1024), + )); + return; + } + } + + setState(() { + final attachment = Attachment( + id: medium.id, + file: file, + type: medium.type == AssetType.image ? 'image' : 'video', + ); + _addAttachments([attachment]); + }); + } + + /// Adds an attachment to the [messageInputController.attachments] map + void _addAttachments(Iterable attachments) { + final limit = widget.attachmentLimit; + final length = + widget.messageInputController.attachments.length + attachments.length; + if (length > limit) { + final onAttachmentLimitExceed = widget.onAttachmentLimitExceeded; + if (onAttachmentLimitExceed != null) { + return onAttachmentLimitExceed( + widget.attachmentLimit, + context.translations.attachmentLimitExceedError(limit), + ); + } + return widget.onError?.call( + context.translations.attachmentLimitExceedError(limit), + ); + } + for (final attachment in attachments) { + widget.messageInputController.addAttachment(attachment); + } + } +} + +class _PickerWidget extends StatefulWidget { + const _PickerWidget({ + Key? key, + required this.filePickerIndex, + required this.containsFile, + required this.selectedMedias, + required this.onAddMoreFilesClick, + required this.onMediaSelected, + required this.streamChatTheme, + }) : super(key: key); + + final int filePickerIndex; + final bool containsFile; + final List selectedMedias; + final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; + final void Function(AssetEntity) onMediaSelected; + final StreamChatThemeData streamChatTheme; + + @override + _PickerWidgetState createState() => _PickerWidgetState(); +} + +class _PickerWidgetState extends State<_PickerWidget> { + Future? requestPermission; + + @override + void initState() { + super.initState(); + requestPermission = PhotoManager.requestPermission(); + } + + @override + Widget build(BuildContext context) { + if (widget.filePickerIndex != 0) { + return const Offstage(); + } + return FutureBuilder( + future: requestPermission, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Offstage(); + } + + if (snapshot.data!) { + if (widget.containsFile) { + return GestureDetector( + onTap: () { + widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); + }, + child: Container( + constraints: const BoxConstraints.expand(), + color: widget.streamChatTheme.colorTheme.inputBg, + alignment: Alignment.center, + child: Text( + context.translations.addMoreFilesLabel, + style: TextStyle( + color: widget.streamChatTheme.colorTheme.accentPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + return MediaListView( + selectedIds: widget.selectedMedias, + onSelect: widget.onMediaSelected, + ); + } + + return InkWell( + onTap: () async { + PhotoManager.openSetting(); + }, + child: Container( + color: widget.streamChatTheme.colorTheme.inputBg, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SvgPicture.asset( + 'svgs/icon_picture_empty_state.svg', + package: 'stream_chat_flutter', + height: 140, + color: widget.streamChatTheme.colorTheme.disabled, + ), + Text( + context.translations.enablePhotoAndVideoAccessMessage, + style: widget.streamChatTheme.textTheme.body.copyWith( + color: widget.streamChatTheme.colorTheme.textLowEmphasis, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Center( + child: Text( + context.translations.allowGalleryAccessMessage, + style: widget.streamChatTheme.textTheme.bodyBold.copyWith( + color: widget.streamChatTheme.colorTheme.accentPrimary, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index f58b7439..da0cd3db 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -29,6 +29,7 @@ export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; export 'src/mip/countdown_button.dart'; +export 'src/mip/stream_attachment_picker.dart'; export 'src/mip/stream_message_send_button.dart'; export 'src/mip/stream_message_text_field.dart'; export 'src/option_list_tile.dart'; From 3465ae3401efab5ba26c38a952187a478b196838 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 3 Nov 2021 17:45:55 +0530 Subject: [PATCH 10/58] analysis --- packages/stream_chat_flutter/lib/src/message_input.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 67785cd2..f9dd8896 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -9,13 +9,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:photo_manager/photo_manager.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/commands_overlay.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/src/emoji_overlay.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/media_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/multi_overlay.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; From 640777f87883e0a1efc12960b1beaea2be07a7a7 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 4 Nov 2021 13:44:49 +0530 Subject: [PATCH 11/58] fix: fixed attachment error --- packages/stream_chat_flutter/lib/src/message_input.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index f9dd8896..7741dade 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1538,7 +1538,9 @@ class MessageInputState extends State { /// Sends the current message Future sendMessage() async { var text = messageInputController.text.trim(); - if (text.isEmpty && messageInputController.attachments.isEmpty) { + var attachments = messageInputController.attachments; + + if (text.isEmpty && attachments.isEmpty) { return; } @@ -1561,7 +1563,7 @@ class MessageInputState extends State { if (widget.editMessage != null) { message = widget.editMessage!.copyWith( text: text, - attachments: messageInputController.attachments, + attachments: attachments, mentionedUsers: messageInputController.mentionedUsers .where((u) => text.contains('@${u.name}')) .toList(), @@ -1570,7 +1572,7 @@ class MessageInputState extends State { message = (widget.initialMessage ?? Message()).copyWith( parentId: widget.parentMessage?.id, text: text, - attachments: messageInputController.attachments, + attachments: attachments, mentionedUsers: messageInputController.mentionedUsers .where((u) => text.contains('@${u.name}')) .toList(), From 267adfd5f0faeef42807dd1e8fa934feae5b8c5f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 4 Nov 2021 13:55:46 +0530 Subject: [PATCH 12/58] fix: fixed slow mode --- packages/stream_chat_flutter/lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 7741dade..9b001791 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1718,7 +1718,7 @@ class MessageInputState extends State { void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); _messageInputTheme = MessageInputTheme.of(context); - if (widget.editMessage == null) _startSlowMode(); + if (widget.editMessage == null && _timeOut <= 0) _startSlowMode(); if ((widget.editMessage != null || widget.initialMessage != null) && !_initialized) { From 1e01db318b0e887f358fc0bb51dfec0ea34339b0 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 4 Nov 2021 14:11:32 +0530 Subject: [PATCH 13/58] fix: fixed slow mode --- packages/stream_chat_flutter/lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 9b001791..6ee5c435 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1538,7 +1538,7 @@ class MessageInputState extends State { /// Sends the current message Future sendMessage() async { var text = messageInputController.text.trim(); - var attachments = messageInputController.attachments; + final attachments = messageInputController.attachments; if (text.isEmpty && attachments.isEmpty) { return; From e973f98308c8ee0b86a3997b9b5ef61c82f413f3 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 4 Nov 2021 16:38:26 +0530 Subject: [PATCH 14/58] feat: Added allowed attachment types --- .../lib/src/mip/stream_attachment_picker.dart | 135 +++++++++++------- 1 file changed, 81 insertions(+), 54 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart index 904825fe..e39efc1c 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart @@ -36,6 +36,8 @@ class StreamAttachmentPicker extends StatefulWidget { /// do not set it if you're using our default CDN final int maxAttachmentSize; + final List allowedAttachmentTypes; + const StreamAttachmentPicker({ Key? key, required this.messageInputController, @@ -49,6 +51,11 @@ class StreamAttachmentPicker extends StatefulWidget { this.compressedVideoFrameRate = 30, this.onChangeInputState, this.onError, + this.allowedAttachmentTypes = const [ + DefaultAttachmentTypes.image, + DefaultAttachmentTypes.file, + DefaultAttachmentTypes.video, + ], }) : super(key: key); @override @@ -125,62 +132,73 @@ class _StreamAttachmentPickerState extends State { children: [ Row( children: [ - IconButton( - icon: StreamSvgIcon.pictures( - color: _getIconColor(0), + if (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.image)) + IconButton( + icon: StreamSvgIcon.pictures( + color: _getIconColor(0), + ), + onPressed: _attachmentContainsFile && + messageInputController.attachments.isNotEmpty + ? null + : () { + setState(() { + _filePickerIndex = 0; + }); + }, ), - onPressed: _attachmentContainsFile && - messageInputController.attachments.isNotEmpty - ? null - : () { - setState(() { - _filePickerIndex = 0; - }); - }, - ), - IconButton( - iconSize: 32, - icon: StreamSvgIcon.files( - color: _getIconColor(1), + if (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.file)) + IconButton( + iconSize: 32, + icon: StreamSvgIcon.files( + color: _getIconColor(1), + ), + onPressed: !_attachmentContainsFile && + messageInputController.attachments.isNotEmpty + ? null + : () { + widget + .onFilePicked(DefaultAttachmentTypes.file); + }, ), - onPressed: !_attachmentContainsFile && - messageInputController.attachments.isNotEmpty - ? null - : () { - widget.onFilePicked(DefaultAttachmentTypes.file); - }, - ), - IconButton( - icon: StreamSvgIcon.camera( - color: _getIconColor(2), + if (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.image)) + IconButton( + icon: StreamSvgIcon.camera( + color: _getIconColor(2), + ), + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && + messageInputController + .attachments.isNotEmpty) + ? null + : () { + widget.onFilePicked( + DefaultAttachmentTypes.image, + camera: true, + ); + }, ), - onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && - messageInputController.attachments.isNotEmpty) - ? null - : () { - widget.onFilePicked( - DefaultAttachmentTypes.image, - camera: true, - ); - }, - ), - IconButton( - padding: const EdgeInsets.all(0), - icon: StreamSvgIcon.record( - color: _getIconColor(3), + if (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.video)) + IconButton( + padding: const EdgeInsets.all(0), + icon: StreamSvgIcon.record( + color: _getIconColor(3), + ), + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && + messageInputController + .attachments.isNotEmpty) + ? null + : () { + widget.onFilePicked( + DefaultAttachmentTypes.video, + camera: true, + ); + }, ), - onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && - messageInputController.attachments.isNotEmpty) - ? null - : () { - widget.onFilePicked( - DefaultAttachmentTypes.video, - camera: true, - ); - }, - ), ], ), DecoratedBox( @@ -205,7 +223,11 @@ class _StreamAttachmentPickerState extends State { ), ), ), - if (widget.isOpen) + if (widget.isOpen && + (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.image) || + (widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.file)))) Expanded( child: DecoratedBox( decoration: BoxDecoration( @@ -229,6 +251,7 @@ class _StreamAttachmentPickerState extends State { _addAssetAttachment(media); } }, + allowedAttachmentTypes: widget.allowedAttachmentTypes, ), ), ), @@ -326,6 +349,7 @@ class _PickerWidget extends StatefulWidget { required this.onAddMoreFilesClick, required this.onMediaSelected, required this.streamChatTheme, + required this.allowedAttachmentTypes, }) : super(key: key); final int filePickerIndex; @@ -334,6 +358,7 @@ class _PickerWidget extends StatefulWidget { final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; final void Function(AssetEntity) onMediaSelected; final StreamChatThemeData streamChatTheme; + final List allowedAttachmentTypes; @override _PickerWidgetState createState() => _PickerWidgetState(); @@ -361,7 +386,9 @@ class _PickerWidgetState extends State<_PickerWidget> { } if (snapshot.data!) { - if (widget.containsFile) { + if (widget.containsFile || + !widget.allowedAttachmentTypes + .contains(DefaultAttachmentTypes.image)) { return GestureDetector( onTap: () { widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); From e72cd812401f75bf5288023209f0c331ac1ca98c Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 4 Nov 2021 17:35:21 +0530 Subject: [PATCH 15/58] feat: Added ability to add custom attachment types --- .../lib/src/mip/stream_attachment_picker.dart | 102 +++++++++++++----- 1 file changed, 78 insertions(+), 24 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart index e39efc1c..ea7d54fb 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/media_list_view.dart'; @@ -15,6 +14,11 @@ typedef FilePickerCallback = void Function( bool camera, }); +typedef CustomAttachmentIconBuilder = Widget Function( + BuildContext context, + bool active, +); + class StreamAttachmentPicker extends StatefulWidget { final bool isOpen; final double pickerSize; @@ -38,6 +42,8 @@ class StreamAttachmentPicker extends StatefulWidget { final List allowedAttachmentTypes; + final List customAttachmentTypes; + const StreamAttachmentPicker({ Key? key, required this.messageInputController, @@ -56,6 +62,7 @@ class StreamAttachmentPicker extends StatefulWidget { DefaultAttachmentTypes.file, DefaultAttachmentTypes.video, ], + this.customAttachmentTypes = const [], }) : super(key: key); @override @@ -70,9 +77,15 @@ class _StreamAttachmentPickerState extends State { var _streamChatTheme = StreamChatTheme.of(context); var messageInputController = widget.messageInputController; + final _attachmentContainsImage = + messageInputController.attachments.any((it) => it.type == 'image'); + final _attachmentContainsFile = messageInputController.attachments.any((it) => it.type == 'file'); + final _attachmentContainsVideo = + messageInputController.attachments.any((it) => it.type == 'video'); + final attachmentLimitCrossed = messageInputController.attachments.length >= widget.attachmentLimit; @@ -80,12 +93,13 @@ class _StreamAttachmentPickerState extends State { final streamChatThemeData = _streamChatTheme; switch (index) { case 0: - return messageInputController.attachments.isEmpty + return _filePickerIndex == 0 || _attachmentContainsImage ? streamChatThemeData.colorTheme.accentPrimary - : (!_attachmentContainsFile + : (_attachmentContainsImage ? streamChatThemeData.colorTheme.accentPrimary - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.2)); + : streamChatThemeData.colorTheme.textHighEmphasis.withOpacity( + messageInputController.attachments.isEmpty ? 0.5 : 0.2, + )); case 1: return _attachmentContainsFile ? streamChatThemeData.colorTheme.accentPrimary @@ -95,7 +109,8 @@ class _StreamAttachmentPickerState extends State { : streamChatThemeData.colorTheme.textHighEmphasis .withOpacity(0.2)); case 2: - return attachmentLimitCrossed + return widget.messageInputController.attachments.isNotEmpty && + (!_attachmentContainsImage || attachmentLimitCrossed) ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) : _attachmentContainsFile && messageInputController.attachments.isNotEmpty @@ -104,7 +119,8 @@ class _StreamAttachmentPickerState extends State { : streamChatThemeData.colorTheme.textHighEmphasis .withOpacity(0.5); case 3: - return attachmentLimitCrossed + return widget.messageInputController.attachments.isNotEmpty && + (!_attachmentContainsVideo || attachmentLimitCrossed) ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) : _attachmentContainsFile && messageInputController.attachments.isNotEmpty @@ -138,14 +154,15 @@ class _StreamAttachmentPickerState extends State { icon: StreamSvgIcon.pictures( color: _getIconColor(0), ), - onPressed: _attachmentContainsFile && - messageInputController.attachments.isNotEmpty - ? null - : () { - setState(() { - _filePickerIndex = 0; - }); - }, + onPressed: + messageInputController.attachments.isNotEmpty && + !_attachmentContainsImage + ? null + : () { + setState(() { + _filePickerIndex = 0; + }); + }, ), if (widget.allowedAttachmentTypes .contains(DefaultAttachmentTypes.file)) @@ -154,8 +171,9 @@ class _StreamAttachmentPickerState extends State { icon: StreamSvgIcon.files( color: _getIconColor(1), ), - onPressed: !_attachmentContainsFile && - messageInputController.attachments.isNotEmpty + onPressed: messageInputController + .attachments.isNotEmpty && + !_attachmentContainsFile ? null : () { widget @@ -169,9 +187,9 @@ class _StreamAttachmentPickerState extends State { color: _getIconColor(2), ), onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && - messageInputController - .attachments.isNotEmpty) + (messageInputController + .attachments.isNotEmpty && + !_attachmentContainsVideo) ? null : () { widget.onFilePicked( @@ -188,9 +206,9 @@ class _StreamAttachmentPickerState extends State { color: _getIconColor(3), ), onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && - messageInputController - .attachments.isNotEmpty) + (messageInputController + .attachments.isNotEmpty && + !_attachmentContainsVideo) ? null : () { widget.onFilePicked( @@ -199,6 +217,26 @@ class _StreamAttachmentPickerState extends State { ); }, ), + for (int i = 0; + i < widget.customAttachmentTypes.length; + i++) + IconButton( + onPressed: () { + if (messageInputController.attachments.isNotEmpty) { + if (!messageInputController.attachments.any((e) => + e.type == + widget.customAttachmentTypes[i].type)) { + return; + } + } + + setState(() { + _filePickerIndex = i + 1; + }); + }, + icon: widget.customAttachmentTypes[i] + .iconBuilder(context, _filePickerIndex == i + 1), + ), ], ), DecoratedBox( @@ -252,6 +290,7 @@ class _StreamAttachmentPickerState extends State { } }, allowedAttachmentTypes: widget.allowedAttachmentTypes, + customAttachmentTypes: widget.customAttachmentTypes, ), ), ), @@ -350,6 +389,7 @@ class _PickerWidget extends StatefulWidget { required this.onMediaSelected, required this.streamChatTheme, required this.allowedAttachmentTypes, + required this.customAttachmentTypes, }) : super(key: key); final int filePickerIndex; @@ -359,6 +399,7 @@ class _PickerWidget extends StatefulWidget { final void Function(AssetEntity) onMediaSelected; final StreamChatThemeData streamChatTheme; final List allowedAttachmentTypes; + final List customAttachmentTypes; @override _PickerWidgetState createState() => _PickerWidgetState(); @@ -376,7 +417,8 @@ class _PickerWidgetState extends State<_PickerWidget> { @override Widget build(BuildContext context) { if (widget.filePickerIndex != 0) { - return const Offstage(); + return widget.customAttachmentTypes[widget.filePickerIndex - 1] + .pickerBuilder(context); } return FutureBuilder( future: requestPermission, @@ -453,3 +495,15 @@ class _PickerWidgetState extends State<_PickerWidget> { ); } } + +class CustomAttachmentType { + String type; + CustomAttachmentIconBuilder iconBuilder; + WidgetBuilder pickerBuilder; + + CustomAttachmentType({ + required this.type, + required this.iconBuilder, + required this.pickerBuilder, + }); +} From 9f32eb89f85c646fc84b343373caed1728ed22c5 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 5 Nov 2021 16:31:26 +0530 Subject: [PATCH 16/58] feat: Added copyWith to stream_attachment_picker.dart --- .../lib/src/message_input.dart | 28 +++++++++---- .../lib/src/mip/stream_attachment_picker.dart | 42 +++++++++++++++++++ 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 6ee5c435..61ac3ac6 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -84,6 +84,13 @@ typedef MessageRelatedBuilder = Widget Function( MessageInputController messageInputController, ); +/// Widget builder for a custom attachment picker. +typedef AttachmentsPickerBuilder = Widget Function( + BuildContext context, + MessageInputController messageInputController, + StreamAttachmentPicker defaultPicker, +); + /// Location for actions on the [MessageInput] enum ActionsLocation { /// Align to left @@ -330,7 +337,7 @@ class MessageInput extends StatefulWidget { final bool mentionAllAppUsers; /// Builds bottom sheet when attachment picker is opened. - final MessageRelatedBuilder? attachmentsPickerBuilder; + final AttachmentsPickerBuilder? attachmentsPickerBuilder; /// Builder for creating send button final MessageRelatedBuilder? sendButtonBuilder; @@ -951,14 +958,7 @@ class MessageInputState extends State { } Widget _buildFilePickerSection() { - if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) { - return widget.attachmentsPickerBuilder!( - context, - messageInputController, - ); - } - - return StreamAttachmentPicker( + final picker = StreamAttachmentPicker( messageInputController: messageInputController, onFilePicked: pickFile, isOpen: _openFilePickerSection, @@ -975,6 +975,16 @@ class MessageInputState extends State { }, onError: _showErrorAlert, ); + + if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) { + return widget.attachmentsPickerBuilder!( + context, + messageInputController, + picker, + ); + } + + return picker; } Widget _buildMentionsOverlayEntry() { diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart index ea7d54fb..f075a09c 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart @@ -9,16 +9,19 @@ import 'package:stream_chat_flutter/src/video_service.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_compress/video_compress.dart'; +/// Callback for when a file has to be picked. typedef FilePickerCallback = void Function( DefaultAttachmentTypes fileType, { bool camera, }); +/// Callback for building an icon for a custom attachment type. typedef CustomAttachmentIconBuilder = Widget Function( BuildContext context, bool active, ); +/// class StreamAttachmentPicker extends StatefulWidget { final bool isOpen; final double pickerSize; @@ -65,6 +68,45 @@ class StreamAttachmentPicker extends StatefulWidget { this.customAttachmentTypes = const [], }) : super(key: key); + StreamAttachmentPicker copyWith({ + Key? key, + MessageInputController? messageInputController, + FilePickerCallback? onFilePicked, + bool? isOpen, + double? pickerSize, + int? attachmentLimit, + AttachmentLimitExceedListener? onAttachmentLimitExceeded, + int? maxAttachmentSize, + VideoQuality? compressedVideoQuality, + int? compressedVideoFrameRate, + ValueChanged? onChangeInputState, + ValueChanged? onError, + List? allowedAttachmentTypes, + List? customAttachmentTypes = const [], + }) => + StreamAttachmentPicker( + key: key ?? this.key, + messageInputController: + messageInputController ?? this.messageInputController, + onFilePicked: onFilePicked ?? this.onFilePicked, + isOpen: isOpen ?? this.isOpen, + pickerSize: pickerSize ?? this.pickerSize, + attachmentLimit: attachmentLimit ?? this.attachmentLimit, + onAttachmentLimitExceeded: + onAttachmentLimitExceeded ?? this.onAttachmentLimitExceeded, + maxAttachmentSize: maxAttachmentSize ?? this.maxAttachmentSize, + compressedVideoQuality: + compressedVideoQuality ?? this.compressedVideoQuality, + compressedVideoFrameRate: + compressedVideoFrameRate ?? this.compressedVideoFrameRate, + onChangeInputState: onChangeInputState ?? this.onChangeInputState, + onError: onError ?? this.onError, + allowedAttachmentTypes: + allowedAttachmentTypes ?? this.allowedAttachmentTypes, + customAttachmentTypes: + customAttachmentTypes ?? this.customAttachmentTypes, + ); + @override State createState() => _StreamAttachmentPickerState(); } From 0b0ff753d49f8ff60af81eaa2ca9c1281892b1ce Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 1 Dec 2021 10:52:45 +0100 Subject: [PATCH 17/58] refactor(ui): improve message input controller --- .../lib/src/core/models/message.dart | 29 +- .../example/lib/tutorial_part_4.dart | 4 +- .../example/lib/tutorial_part_6.dart | 4 +- .../stream_chat_flutter/example/pubspec.yaml | 9 +- .../lib/src/localization/translations.dart | 2 +- .../lib/src/message_actions_modal.dart | 4 +- .../countdown_button.dart | 0 .../{ => message_input}/message_input.dart | 302 +++++++----------- .../stream_attachment_picker.dart | 4 +- .../stream_message_send_button.dart | 6 +- .../stream_message_text_field.dart | 2 + .../lib/src/stream_chat_theme.dart | 2 +- .../lib/stream_chat_flutter.dart | 10 +- .../lib/src/message_input_controller.dart | 104 ++++-- 14 files changed, 241 insertions(+), 241 deletions(-) rename packages/stream_chat_flutter/lib/src/{mip => message_input}/countdown_button.dart (100%) rename packages/stream_chat_flutter/lib/src/{ => message_input}/message_input.dart (87%) rename packages/stream_chat_flutter/lib/src/{mip => message_input}/stream_attachment_picker.dart (99%) rename packages/stream_chat_flutter/lib/src/{mip => message_input}/stream_message_send_button.dart (92%) rename packages/stream_chat_flutter/lib/src/{mip => message_input}/stream_message_text_field.dart (99%) diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 10ff713e..85bf8a96 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -58,7 +58,7 @@ class Message extends Equatable { this.ownReactions, this.parentId, this.quotedMessage, - this.quotedMessageId, + String? quotedMessageId, this.replyCount = 0, this.threadParticipants, this.showInChannel, @@ -72,16 +72,19 @@ class Message extends Equatable { this.pinnedBy, this.extraData = const {}, this.deletedAt, - this.status = MessageSendingStatus.sent, + this.status = MessageSendingStatus.sending, this.i18n, }) : id = id ?? const Uuid().v4(), pinExpires = pinExpires?.toUtc(), - createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + _createdAt = createdAt, + _updatedAt = updatedAt, + _quotedMessageId = quotedMessageId; /// Create a new instance from a json factory Message.fromJson(Map json) => _$MessageFromJson( Serializer.moveToExtraDataFromRoot(json, topLevelFields), + ).copyWith( + status: MessageSendingStatus.sent, ); /// The message ID. This is either created by Stream or set client side when @@ -134,8 +137,10 @@ class Message extends Equatable { @JsonKey(toJson: Serializer.readOnly) final Message? quotedMessage; + final String? _quotedMessageId; + /// The ID of the quoted message, if the message is a quoted reply. - final String? quotedMessageId; + String? get quotedMessageId => _quotedMessageId ?? quotedMessage?.id; /// Reserved field indicating the number of replies for this message. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @@ -162,13 +167,17 @@ class Message extends Equatable { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? command; + final DateTime? _createdAt; + /// Reserved field indicating when the message was created. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime createdAt; + DateTime get createdAt => _createdAt ?? DateTime.now(); + + final DateTime? _updatedAt; /// Reserved field indicating when the message was updated last time. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime updatedAt; + DateTime get updatedAt => _updatedAt ?? DateTime.now(); /// User who sent the message @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @@ -301,17 +310,17 @@ class Message extends Equatable { ownReactions: ownReactions ?? this.ownReactions, parentId: parentId ?? this.parentId, quotedMessage: quotedMessage ?? this.quotedMessage, - quotedMessageId: quotedMessageId ?? this.quotedMessageId, + quotedMessageId: quotedMessageId ?? _quotedMessageId, replyCount: replyCount ?? this.replyCount, threadParticipants: threadParticipants ?? this.threadParticipants, showInChannel: showInChannel ?? this.showInChannel, command: command ?? this.command, - createdAt: createdAt ?? this.createdAt, + createdAt: createdAt ?? _createdAt, silent: silent ?? this.silent, extraData: extraData ?? this.extraData, user: user ?? this.user, shadowed: shadowed ?? this.shadowed, - updatedAt: updatedAt ?? this.updatedAt, + updatedAt: updatedAt ?? _updatedAt, deletedAt: deletedAt ?? this.deletedAt, status: status ?? this.status, pinned: pinned ?? this.pinned, diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index 5a05e4f9..ea9afa2f 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -126,7 +126,9 @@ class ThreadPage extends StatelessWidget { ), ), MessageInput( - parentMessage: parent, + messageInputController: MessageInputController( + message: Message(parentId: parent!.id), + ), ), ], ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index 19d5d1ae..2555e007 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -166,7 +166,9 @@ class ThreadPage extends StatelessWidget { ), ), MessageInput( - parentMessage: parent, + messageInputController: MessageInputController( + message: Message(parentId: parent!.id), + ), ), ], ), diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 9977c198..bd785a29 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -27,9 +27,12 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter: ^2.2.1 - stream_chat_localizations: ^1.1.0 - stream_chat_persistence: ^2.2.0 + stream_chat_flutter: + path: ../ + stream_chat_localizations: + path: ../../stream_chat_localizations + stream_chat_persistence: + path: ../../stream_chat_persistence dev_dependencies: flutter_test: diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 8aa8d071..8039ac47 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -1,6 +1,6 @@ import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/connection_status_builder.dart'; -import 'package:stream_chat_flutter/src/message_input.dart'; +import 'package:stream_chat_flutter/src/message_input/message_input.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/message_search_list_view.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index a67004af..e85e7cd8 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -610,7 +610,9 @@ class _MessageActionsModalState extends State { widget.editMessageInputBuilder!(context, widget.message) else MessageInput( - editMessage: widget.message, + messageInputController: MessageInputController( + message: widget.message, + ), preMessageSending: (m) { FocusScope.of(context).unfocus(); Navigator.pop(context); diff --git a/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart b/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/mip/countdown_button.dart rename to packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart similarity index 87% rename from packages/stream_chat_flutter/lib/src/message_input.dart rename to packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 2e035df0..aa18fce5 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -184,12 +184,9 @@ class MessageInput extends StatefulWidget { Key? key, this.onMessageSent, this.preMessageSending, - this.parentMessage, - this.editMessage, this.maxHeight = 150, this.keyboardType = TextInputType.multiline, this.disableAttachments = false, - this.initialMessage, this.messageInputController, this.actions = const [], this.actionsLocation = ActionsLocation.left, @@ -219,18 +216,11 @@ class MessageInput extends StatefulWidget { this.attachmentsPickerBuilder, this.sendButtonBuilder, this.shouldKeepFocusAfterMessage, - }) : assert( - initialMessage == null || editMessage == null, - "Can't provide both `initialMessage` and `editMessage`", - ), - super(key: key); + }) : super(key: key); /// List of options for showing overlays final List customOverlays; - /// Message to edit - final Message? editMessage; - /// Video quality to use when compressing the videos final VideoQuality compressedVideoQuality; @@ -242,9 +232,6 @@ class MessageInput extends StatefulWidget { /// do not set it if you're using our default CDN final int maxAttachmentSize; - /// Message to start with - final Message? initialMessage; - /// Function called after sending the message final void Function(Message)? onMessageSent; @@ -252,9 +239,6 @@ class MessageInput extends StatefulWidget { /// Use this to transform the message final FutureOr Function(Message)? preMessageSending; - /// Parent message in case of a thread - final Message? parentMessage; - /// Maximum Height for the TextField to grow before it starts scrolling final double maxHeight; @@ -367,11 +351,10 @@ class MessageInputState extends State { final _imagePicker = ImagePicker(); late final _focusNode = widget.focusNode ?? FocusNode(); bool _inputEnabled = true; - bool _commandEnabled = false; + bool get _commandEnabled => messageInputController.value.command != null; bool _showCommandsOverlay = false; bool _showMentionsOverlay = false; - Command? _chosenCommand; bool _actionsShrunk = false; bool _openFilePickerSection = false; @@ -382,16 +365,17 @@ class MessageInputState extends State { late StreamChatThemeData _streamChatTheme; late MessageInputThemeData _messageInputTheme; - bool get _hasQuotedMessage => widget.quotedMessage != null; + bool get _hasQuotedMessage => + messageInputController.value.quotedMessage != null; bool get _messageIsPresent => messageInputController.text.trim().isNotEmpty; + bool get _isEditing => + messageInputController.value.status != MessageSendingStatus.sending; + @override void initState() { super.initState(); - if (widget.editMessage != null || widget.initialMessage != null) { - _parseExistingMessage(widget.editMessage ?? widget.initialMessage!); - } messageInputController.textEditingController .addListener(_onChangedDebounced); _focusNode.addListener(_focusNodeListener); @@ -437,70 +421,74 @@ class MessageInputState extends State { Widget build(BuildContext context) { Widget child = ValueListenableBuilder( valueListenable: messageInputController, - builder: (context, value, wid) => DecoratedBox( - decoration: BoxDecoration( - color: _messageInputTheme.inputBackgroundColor, - ), - child: SafeArea( - child: GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy > 0) { - _focusNode.unfocus(); - if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - }); + builder: (context, value, wid) { + print('VALUE ${value.text}'); + return DecoratedBox( + decoration: BoxDecoration( + color: _messageInputTheme.inputBackgroundColor, + ), + child: SafeArea( + child: GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy > 0) { + _focusNode.unfocus(); + if (_openFilePickerSection) { + setState(() { + _openFilePickerSection = false; + }); + } } - } - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (_hasQuotedMessage) - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon.reply( - color: _streamChatTheme.colorTheme.disabled, + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_hasQuotedMessage) + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon.reply( + color: _streamChatTheme.colorTheme.disabled, + ), ), - ), - Text( - context.translations.replyToMessageLabel, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: StreamSvgIcon.closeSmall(), - onPressed: widget.onQuotedMessageCleared, - ), - ], + Text( + context.translations.replyToMessageLabel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: widget.onQuotedMessageCleared, + ), + ], + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: _buildTextField(context), - ), - if (widget.parentMessage != null && !widget.hideSendAsDm) Padding( - padding: const EdgeInsets.only( - right: 12, - left: 12, - bottom: 12, - ), - child: _buildDmCheckbox(), + padding: const EdgeInsets.symmetric(vertical: 8), + child: _buildTextField(context), ), - _buildFilePickerSection(), - ], + if (messageInputController.value.parentId != null && + !widget.hideSendAsDm) + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + bottom: 12, + ), + child: _buildDmCheckbox(), + ), + _buildFilePickerSection(), + ], + ), ), ), - ), - ), + ); + }, ); - if (widget.editMessage == null) { + if (_isEditing) { child = Material( elevation: 8, child: child, @@ -575,10 +563,8 @@ class MessageInputState extends State { : _streamChatTheme.colorTheme.barsBg, child: InkWell( onTap: () { - setState(() { - messageInputController.showInChannel = - !messageInputController.showInChannel; - }); + messageInputController.showInChannel = + !messageInputController.showInChannel; }, child: AnimatedCrossFade( duration: const Duration(milliseconds: 300), @@ -621,7 +607,7 @@ class MessageInputState extends State { onSendMessage: sendMessage, timeOut: _timeOut, isIdle: !_messageIsPresent && messageInputController.attachments.isEmpty, - isEditEnabled: widget.editMessage != null, + isEditEnabled: _isEditing, idleSendButton: widget.idleSendButton, activeSendButton: widget.activeSendButton, ); @@ -668,7 +654,7 @@ class MessageInputState extends State { if (!widget.disableAttachments) _buildAttachmentButton(context), if (widget.showCommandsButton && - widget.editMessage == null && + !_isEditing && channel.state != null && channel.config?.commands.isNotEmpty == true) _buildCommandButton(context), @@ -791,7 +777,7 @@ class MessageInputState extends State { size: 16, ), Text( - _chosenCommand?.name.toUpperCase() ?? '', + messageInputController.value.command!.toUpperCase(), style: _streamChatTheme.textTheme.footnoteBold.copyWith( color: Colors.white, @@ -825,9 +811,7 @@ class MessageInputState extends State { height: 24, width: 24, ), - onPressed: () { - setState(() => _commandEnabled = false); - }, + onPressed: messageInputController.clear, ), ), if (!_commandEnabled && @@ -848,8 +832,10 @@ class MessageInputState extends State { final channel = StreamChannel.of(context).channel; if (value.isNotEmpty) { - // ignore: no-empty-block - channel.keyStroke(widget.parentMessage?.id).catchError((e) {}); + channel + .keyStroke(messageInputController.value.parentId) + // ignore: no-empty-block + .catchError((e) {}); } var actionsLength = widget.actions.length; @@ -869,7 +855,7 @@ class MessageInputState extends State { ); String _getHint(BuildContext context) { - if (_commandEnabled && _chosenCommand!.name == 'giphy') { + if (_commandEnabled && messageInputController.value.command == 'giphy') { return context.translations.searchGifLabel; } if (messageInputController.attachments.isNotEmpty) { @@ -1034,15 +1020,11 @@ class MessageInputState extends State { splits[splits.length - 1] = user.name; final rejoin = splits.join('@'); - messageInputController.textEditingController.value = TextEditingValue( - text: rejoin + - messageInputController.text.substring( - messageInputController.selectionStart, - ), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); + messageInputController.text = rejoin + + messageInputController.text.substring( + messageInputController.selectionStart, + ); + _onChangedDebounced.cancel(); setState(() => _showMentionsOverlay = false); }, @@ -1077,22 +1059,17 @@ class MessageInputState extends State { void _chooseEmoji(List splits, Emoji emoji) { final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; - messageInputController.textEditingController.value = TextEditingValue( - text: rejoin + - messageInputController.text.substring( - messageInputController.selectionStart, - ), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); + messageInputController.text = rejoin + + messageInputController.text.substring( + messageInputController.selectionStart, + ); } void _setCommand(Command c) { - messageInputController.clear(); + messageInputController + ..clear() + ..command = c; setState(() { - _chosenCommand = c; - _commandEnabled = true; _showCommandsOverlay = false; }); } @@ -1133,9 +1110,7 @@ class MessageInputState extends State { (e) => ClipRRect( borderRadius: BorderRadius.circular(10), child: FileAttachment( - message: Message( - status: MessageSendingStatus.sending, - ), // dummy message + message: Message(), // dummy message attachment: e, size: Size( MediaQuery.of(context).size.width * 0.65, @@ -1202,9 +1177,11 @@ class MessageInputState extends State { focusElevation: 0, hoverElevation: 0, onPressed: () { - setState( - () => messageInputController.attachments - .removeWhere((e) => e.id == attachment.id), + messageInputController.value = + messageInputController.value.copyWith( + attachments: messageInputController.attachments + .where((it) => it.id != attachment.id) + .toList(), ); }, fillColor: @@ -1412,7 +1389,7 @@ class MessageInputState extends State { /// /// Note: Only meant to be used from outside the state. void addAttachment(Attachment attachment) { - setState(() => _addAttachments([attachment])); + _addAttachments([attachment]); } /// Adds an attachment to the [messageInputController.attachments] map @@ -1539,23 +1516,20 @@ class MessageInputState extends State { } } - setState(() { - _addAttachments([ - attachment.copyWith( - file: file, - extraData: {...attachment.extraData} - ..update('file_size', ((_) => file!.size!)), - ), - ]); - }); + _addAttachments([ + attachment.copyWith( + file: file, + extraData: {...attachment.extraData} + ..update('file_size', ((_) => file!.size!)), + ), + ]); } /// Sends the current message Future sendMessage() async { - var text = messageInputController.text.trim(); - final attachments = messageInputController.attachments; + var message = messageInputController.value; - if (text.isEmpty && attachments.isEmpty) { + if (messageInputController.isValid) { return; } @@ -1563,48 +1537,9 @@ class MessageInputState extends State { shouldKeepFocus ??= !_commandEnabled; - if (_commandEnabled) { - text = '${'/${_chosenCommand!.name} '}$text'; - } - - messageInputController - ..text = '' - ..clearAttachments(); + messageInputController.reset(); widget.onQuotedMessageCleared?.call(); - setState(() { - _commandEnabled = false; - }); - - Message message; - if (widget.editMessage != null) { - message = widget.editMessage!.copyWith( - text: text, - attachments: attachments, - mentionedUsers: messageInputController.mentionedUsers - .where((u) => text.contains('@${u.name}')) - .toList(), - ); - } else { - message = (widget.initialMessage ?? Message()).copyWith( - parentId: widget.parentMessage?.id, - text: text, - attachments: attachments, - mentionedUsers: messageInputController.mentionedUsers - .where((u) => text.contains('@${u.name}')) - .toList(), - showInChannel: widget.parentMessage != null - ? messageInputController.showInChannel - : null, - ); - } - - if (widget.quotedMessage != null) { - message = message.copyWith( - quotedMessageId: widget.quotedMessage!.id, - ); - } - if (widget.preMessageSending != null) { message = await widget.preMessageSending!(message); } @@ -1615,13 +1550,9 @@ class MessageInputState extends State { await streamChannel.reloadChannel(); } - messageInputController.clearMentionedUsers(); - try { Future sendingFuture; - if (widget.editMessage == null || - widget.editMessage!.status == MessageSendingStatus.failed || - widget.editMessage!.status == MessageSendingStatus.sending) { + if (!_isEditing) { sendingFuture = channel.sendMessage(message); } else { sendingFuture = channel.updateMessage(message); @@ -1635,7 +1566,7 @@ class MessageInputState extends State { final resp = await sendingFuture; if (resp.message?.type == 'error') { - _parseExistingMessage(message); + messageInputController.value = message; } _startSlowMode(); widget.onMessageSent?.call(resp.message); @@ -1714,12 +1645,6 @@ class MessageInputState extends State { ); } - void _parseExistingMessage(Message message) { - final messageText = message.text; - if (messageText != null) messageInputController.text = messageText; - _addAttachments(message.attachments); - } - @override void dispose() { messageInputController.textEditingController @@ -1731,19 +1656,12 @@ class MessageInputState extends State { super.dispose(); } - bool _initialized = false; - @override void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); _messageInputTheme = MessageInputTheme.of(context); - if (widget.editMessage == null && _timeOut <= 0) _startSlowMode(); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); - if ((widget.editMessage != null || widget.initialMessage != null) && - !_initialized) { - FocusScope.of(context).requestFocus(_focusNode); - _initialized = true; - } super.didChangeDependencies(); } } diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart similarity index 99% rename from packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart index f075a09c..066ed780 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart @@ -116,8 +116,8 @@ class _StreamAttachmentPickerState extends State { @override Widget build(BuildContext context) { - var _streamChatTheme = StreamChatTheme.of(context); - var messageInputController = widget.messageInputController; + final _streamChatTheme = StreamChatTheme.of(context); + final messageInputController = widget.messageInputController; final _attachmentContainsImage = messageInputController.attachments.any((it) => it.type == 'image'); diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart similarity index 92% rename from packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart index 5443fb18..ec03a58c 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart @@ -23,7 +23,7 @@ class StreamMessageSendButton extends StatelessWidget { @override Widget build(BuildContext context) { - var _streamChatTheme = StreamChatTheme.of(context); + final _streamChatTheme = StreamChatTheme.of(context); late Widget sendButton; if (timeOut > 0) { @@ -46,7 +46,7 @@ class StreamMessageSendButton extends StatelessWidget { } Widget _buildIdleSendButton(BuildContext context) { - var _messageInputTheme = MessageInputTheme.of(context); + final _messageInputTheme = MessageInputTheme.of(context); return Padding( padding: const EdgeInsets.all(8), @@ -58,7 +58,7 @@ class StreamMessageSendButton extends StatelessWidget { } Widget _buildSendButton(BuildContext context) { - var _messageInputTheme = MessageInputTheme.of(context); + final _messageInputTheme = MessageInputTheme.of(context); return Padding( padding: const EdgeInsets.all(8), diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart similarity index 99% rename from packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index 43acce31..37b4f683 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -1,3 +1,5 @@ +// ignore_for_file: prefer-trailing-comma, cascade_invocations + import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:flutter/cupertino.dart'; diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 23d913cb..f3818b2e 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide TextTheme; import 'package:stream_chat_flutter/src/channel_preview.dart'; import 'package:stream_chat_flutter/src/gradient_avatar.dart'; -import 'package:stream_chat_flutter/src/message_input.dart'; +import 'package:stream_chat_flutter/src/message_input/message_input.dart'; import 'package:stream_chat_flutter/src/reaction_icon.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 78977cf4..b39a2eea 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -23,16 +23,16 @@ export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; export 'src/mention_tile.dart'; export 'src/message_action.dart'; -export 'src/message_input.dart'; +export 'src/message_input/message_input.dart'; export 'src/message_list_view.dart'; export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; -export 'src/mip/countdown_button.dart'; -export 'src/mip/stream_attachment_picker.dart'; -export 'src/mip/stream_message_send_button.dart'; -export 'src/mip/stream_message_text_field.dart'; +export 'src/message_input/countdown_button.dart'; +export 'src/message_input/stream_attachment_picker.dart'; +export 'src/message_input/stream_message_send_button.dart'; +export 'src/message_input/stream_message_text_field.dart'; export 'src/option_list_tile.dart'; export 'src/reaction_icon.dart'; export 'src/reaction_picker.dart'; diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 3339bc5b..4d1565da 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -4,29 +4,82 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat/stream_chat.dart'; +typedef MessageValidator = bool Function(Message message); + /// Controller for storing and mutating a [Message] value. class MessageInputController extends ValueNotifier { /// Creates a controller for an editable text field. /// /// This constructor treats a null [message] argument as if it were the empty /// message. - factory MessageInputController({Message? message}) => - MessageInputController._(message ?? Message()); + factory MessageInputController({ + Message? message, + MessageValidator? validator, + }) => + MessageInputController._( + initialMessage: message ?? Message(), + validator: validator ?? _defaultValidator, + ); /// Creates a controller for an editable text field from an initial [text]. - factory MessageInputController.fromText(String? text) => - MessageInputController._(Message(text: text)); + factory MessageInputController.fromText( + String? text, { + MessageValidator? validator, + }) => + MessageInputController._( + initialMessage: Message(text: text), + validator: validator ?? _defaultValidator, + ); /// Creates a controller for an editable text field from an initial /// [attachments]. factory MessageInputController.fromAttachments( - List attachments, - ) => - MessageInputController._(Message(attachments: attachments)); + List attachments, { + MessageValidator? validator, + }) => + MessageInputController._( + initialMessage: Message(attachments: attachments), + validator: validator ?? _defaultValidator, + ); - MessageInputController._(Message message) - : _textEditingController = TextEditingController(text: message.text), - super(message); + MessageInputController._({ + required Message initialMessage, + this.validator = _defaultValidator, + }) : _textEditingController = + TextEditingController(text: initialMessage.text), + _initialMessage = initialMessage, + super(initialMessage) { + addListener(_textEditingSyncer); + } + + /// A callback function that validates the message. + final MessageValidator validator; + + /// Checks if the message is valid. + bool get isValid => validator(value); + + static bool _defaultValidator(Message message) => + message.text?.isNotEmpty != true && message.attachments.isEmpty; + + void _textEditingSyncer() { + final cleanText = value.command == null + ? value.text + : value.text?.replaceFirst( + '/${value.command} ', + '', + ); + + if (cleanText != _textEditingController.text) { + final previousOffset = _textEditingController.value.selection.start; + final previousText = _textEditingController.text; + final diff = (cleanText?.length ?? 0) - previousText.length; + _textEditingController + ..text = cleanText ?? '' + ..selection = TextSelection.collapsed( + offset: previousOffset + diff, + ); + } + } /// TextEditingController get textEditingController => _textEditingController; @@ -35,24 +88,29 @@ class MessageInputController extends ValueNotifier { /// String get text => _textEditingController.text; + final Message _initialMessage; + /// set message(Message message) { value = message; } - set text(String newText) { - value = value.copyWith(text: newText); - _textEditingController - ..text = newText - ..selection = TextSelection.fromPosition( - TextPosition(offset: _textEditingController.text.length), - ); + /// + set command(Command command) { + value = value.copyWith( + command: command.name, + text: '/${command.name} ', + ); } - /// - set textEditingValue(TextEditingValue newValue) { - _textEditingController.value = newValue; - value = value.copyWith(text: _textEditingController.text); + set text(String newText) { + var newTextWithCommand = newText; + if (value.command != null) { + if (!newText.startsWith('/${value.command}')) { + newTextWithCommand = '/${value.command} $newText'; + } + } + value = value.copyWith(text: newTextWithCommand); } /// @@ -147,9 +205,13 @@ class MessageInputController extends ValueNotifier { _textEditingController.clear(); } + /// Set the [value] to the initial [Message] value. + void reset() => value = _initialMessage; + @override void dispose() { super.dispose(); + removeListener(_textEditingSyncer); _textEditingController.dispose(); } } From 9e9ed0667b3cf46b5f6858a2f23ed347a429db3e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 3 Dec 2021 16:23:45 +0100 Subject: [PATCH 18/58] chore(llc): update changelog and bump version --- packages/stream_chat/CHANGELOG.md | 2 +- packages/stream_chat/lib/version.dart | 2 +- packages/stream_chat/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 3f20271f..940d25c5 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 3.3.1 🐞 Fixed diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index cc19a375..ed6d9b09 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '3.3.0'; +const PACKAGE_VERSION = '3.3.1'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 38ee620c..599b6ed8 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 3.3.0 +version: 3.3.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues From 28c05e7afc321acdf24f03140f93d80afb66f085 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 3 Dec 2021 16:24:27 +0100 Subject: [PATCH 19/58] chore(core): update changelog and bump version --- packages/stream_chat_flutter_core/CHANGELOG.md | 4 ++++ packages/stream_chat_flutter_core/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 9d0731aa..b69c4262 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.3.1 + +- Updated `stream_chat` dependency to [`3.3.1`](https://pub.dev/packages/stream_chat/changelog). + ## 3.3.0 - Updated `stream_chat` dependency to [`3.3.0`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 48d104f3..8f0718f4 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 3.3.0 +version: 3.3.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^3.3.0 + stream_chat: ^3.3.1 dev_dependencies: dart_code_metrics: ^4.4.0 From 502bb19e095b14d19fa24cee5c4189f1339469d1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 3 Dec 2021 16:25:34 +0100 Subject: [PATCH 20/58] chore(ui): update changelog and bump version --- packages/stream_chat_flutter/CHANGELOG.md | 4 ++++ packages/stream_chat_flutter/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 620ff07b..383eeb02 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.3.2 + +- Updated `stream_chat_flutter_core` dependency to [`3.3.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 3.3.1 ✅ Added diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 40894d3a..86dcd0f6 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 3.3.1 +version: 3.3.2 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -36,7 +36,7 @@ dependencies: rxdart: ^0.27.0 share_plus: ^3.0.4 shimmer: ^2.0.0 - stream_chat_flutter_core: ^3.3.0 + stream_chat_flutter_core: ^3.3.1 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 From 48dbeb3f9f95a306f32c555665dcba609e571fe4 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 8 Dec 2021 13:19:32 +0530 Subject: [PATCH 21/58] update connectivity deps --- packages/stream_chat_flutter_core/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 48d104f3..648d838c 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: collection: ^1.15.0 - connectivity_plus: ^2.0.2 + connectivity_plus: ^2.1.0 flutter: sdk: flutter meta: ^1.3.0 From ff2c8801d5a515220154f89e35b1236bec15aeaf Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 10 Dec 2021 10:00:30 +0100 Subject: [PATCH 22/58] minor updates --- .../lib/src/client/retry_queue.dart | 1 + packages/stream_chat/lib/stream_chat.dart | 1 + .../lib/src/message_input/message_input.dart | 119 +++++++++--------- .../lib/src/message_input_controller.dart | 13 +- 4 files changed, 70 insertions(+), 64 deletions(-) diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index e1b6df70..7c3157b2 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -118,6 +118,7 @@ class RetryQueue { } catch (e) { if (e is! StreamChatNetworkError || !e.isRetriable) { _messageQueue.removeMessage(message); + _sendFailedEvent(message); return true; } // retry logic diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 571d78f5..367b27af 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -7,6 +7,7 @@ export 'package:dio/src/options.dart'; export 'package:dio/src/options.dart' show ProgressCallback; export 'package:logging/logging.dart' show Logger, Level; export 'package:rate_limiter/rate_limiter.dart'; +export 'package:uuid/uuid.dart'; export './src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader; diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index aa18fce5..56bedd57 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -421,72 +421,69 @@ class MessageInputState extends State { Widget build(BuildContext context) { Widget child = ValueListenableBuilder( valueListenable: messageInputController, - builder: (context, value, wid) { - print('VALUE ${value.text}'); - return DecoratedBox( - decoration: BoxDecoration( - color: _messageInputTheme.inputBackgroundColor, - ), - child: SafeArea( - child: GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy > 0) { - _focusNode.unfocus(); - if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - }); - } + 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, - ), - ), - Text( - context.translations.replyToMessageLabel, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: StreamSvgIcon.closeSmall(), - onPressed: widget.onQuotedMessageCleared, - ), - ], - ), - ), + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_hasQuotedMessage) Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: _buildTextField(context), - ), - if (messageInputController.value.parentId != null && - !widget.hideSendAsDm) - Padding( - padding: const EdgeInsets.only( - right: 12, - left: 12, - bottom: 12, - ), - child: _buildDmCheckbox(), + 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, + ), + ], ), - _buildFilePickerSection(), - ], - ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: _buildTextField(context), + ), + if (messageInputController.value.parentId != null && + !widget.hideSendAsDm) + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + bottom: 12, + ), + child: _buildDmCheckbox(), + ), + _buildFilePickerSection(), + ], ), ), - ); - }, + ), + ), ); if (_isEditing) { child = Material( diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 4d1565da..0c6a8719 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -88,7 +88,7 @@ class MessageInputController extends ValueNotifier { /// String get text => _textEditingController.text; - final Message _initialMessage; + Message _initialMessage; /// set message(Message message) { @@ -206,13 +206,20 @@ class MessageInputController extends ValueNotifier { } /// Set the [value] to the initial [Message] value. - void reset() => value = _initialMessage; + void reset({bool resetId = true}) { + if (resetId) { + _initialMessage = _initialMessage.copyWith( + id: const Uuid().v4(), + ); + } + value = _initialMessage; + } @override void dispose() { - super.dispose(); removeListener(_textEditingSyncer); _textEditingController.dispose(); + super.dispose(); } } From 0ec6e1c40fe794cc8d13923f9467834a96c98cd9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 10 Dec 2021 15:15:58 +0100 Subject: [PATCH 23/58] minor updates --- .../lib/src/message_input/message_input.dart | 4 +- .../lib/src/message_input_controller.dart | 50 ++++++++++++------- .../stream_chat_flutter_core/pubspec.yaml | 2 +- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 56bedd57..5a3e51e8 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -419,9 +419,9 @@ class MessageInputState extends State { @override Widget build(BuildContext context) { - Widget child = ValueListenableBuilder( + Widget child = MessageValueListenableBuilder( valueListenable: messageInputController, - builder: (context, value, wid) => DecoratedBox( + builder: (context, value, _) => DecoratedBox( decoration: BoxDecoration( color: _messageInputTheme.inputBackgroundColor, ), diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 0c6a8719..59dfbff7 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -4,6 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat/stream_chat.dart'; +/// A value listenable builder related to a [Message] +typedef MessageValueListenableBuilder = ValueListenableBuilder; + +/// A function that returns true if the message is valid and can be sent. typedef MessageValidator = bool Function(Message message); /// Controller for storing and mutating a [Message] value. @@ -81,21 +85,24 @@ class MessageInputController extends ValueNotifier { } } - /// + /// Returns the current message associated with this controller. + Message get message => value; + + /// Returns the controller of the text field linked to this controller. TextEditingController get textEditingController => _textEditingController; final TextEditingController _textEditingController; - /// + /// Returns the text of the message. String get text => _textEditingController.text; Message _initialMessage; - /// + /// Sets the message. set message(Message message) { value = message; } - /// + /// Sets a command for the message. set command(Command command) { value = value.copyWith( command: command.name, @@ -103,6 +110,7 @@ class MessageInputController extends ValueNotifier { ); } + /// Sets the text of the message. set text(String newText) { var newTextWithCommand = newText; if (value.command != null) { @@ -113,79 +121,83 @@ class MessageInputController extends ValueNotifier { value = value.copyWith(text: newTextWithCommand); } - /// + /// Returns the baseOffset of the text field. int get baseOffset => textEditingController.selection.baseOffset; - /// + /// Returns the start of the selection of the text field. int get selectionStart => textEditingController.selection.start; + /// Sets the showInChannel flag of the message. set showInChannel(bool newValue) { value = value.copyWith(showInChannel: newValue); } - /// + /// Returns true if the message is in a thread and + /// should be shown in the main channel as well. bool get showInChannel => value.showInChannel ?? false; - /// + /// Returns the attachments of the message. List get attachments => value.attachments; + /// Sets the list of [attachments] for the message. set attachments(List attachments) { value = value.copyWith(attachments: attachments); } - /// + /// Adds a new attachment to the message. void addAttachment(Attachment attachment) { attachments = [...attachments, attachment]; } - /// + /// Adds a new attachment at the specified [index]. void addAttachmentAt(int index, Attachment attachment) { attachments = [...attachments]..insert(index, attachment); } - /// + /// Removes the specified [attachment] from the message. void removeAttachment(Attachment attachment) { attachments = [...attachments]..remove(attachment); } - /// + /// Remove the attachment with the given [attachmentId]. void removeAttachmentById(String attachmentId) { attachments = [...attachments]..removeWhere((it) => it.id == attachmentId); } - /// + /// Removes the attachment at the given [index]. void removeAttachmentAt(int index) { attachments = [...attachments]..removeAt(index); } - /// + /// Clears the message attachments. void clearAttachments() { attachments = []; } - /// + /// Returns the list of mentioned users in the message. List get mentionedUsers => value.mentionedUsers; + /// Sets the mentioned users. set mentionedUsers(List users) { value = value.copyWith(mentionedUsers: users); } - /// + /// Adds a user to the list of mentioned users. void addMentionedUser(User user) { mentionedUsers = [...mentionedUsers, user]; } - /// + /// Removes the specified [user] from the mentioned users list. void removeMentionedUser(User user) { mentionedUsers = [...mentionedUsers]..remove(user); } - /// + /// Removes the mentioned user with the given [userId]. void removeMentionedUserById(String userId) { mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId); } - /// + /// Removes all mentioned users from the message. void clearMentionedUsers() { mentionedUsers = []; } diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 1b1734be..fb9307c1 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.14.0 <3.0.0' flutter: ">=1.17.0" dependencies: From b39c013e3584c9786b76fef04d2aa6b3732c30c4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 10 Dec 2021 15:53:28 +0100 Subject: [PATCH 24/58] add basic test and fix default validator --- .../lib/src/message_input_controller.dart | 2 +- .../test/message_input_controller_test.dart | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/stream_chat_flutter_core/test/message_input_controller_test.dart diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 59dfbff7..3944d59b 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -63,7 +63,7 @@ class MessageInputController extends ValueNotifier { bool get isValid => validator(value); static bool _defaultValidator(Message message) => - message.text?.isNotEmpty != true && message.attachments.isEmpty; + message.text?.isNotEmpty == true || message.attachments.isNotEmpty; void _textEditingSyncer() { final cleanText = value.command == null diff --git a/packages/stream_chat_flutter_core/test/message_input_controller_test.dart b/packages/stream_chat_flutter_core/test/message_input_controller_test.dart new file mode 100644 index 00000000..842a58bb --- /dev/null +++ b/packages/stream_chat_flutter_core/test/message_input_controller_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +void main() { + testWidgets( + 'should instantiate a new MessageInputController with default validator' + ' and empty message', + (tester) async { + final controller = MessageInputController(); + + expect(controller.isValid, false); + controller.text = 'test'; + expect(controller.isValid, true); + }, + ); + + testWidgets( + 'should instantiate a new MessageInputController with default validator' + ' and specified message', + (tester) async { + final message = Message(text: 'test'); + final controller = MessageInputController( + message: message, + ); + + expect(controller.message, message); + expect(controller.isValid, true); + }, + ); +} From 42ed167091882a0dd428765ab69850485361431a Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Fri, 10 Dec 2021 17:35:58 +0100 Subject: [PATCH 25/58] chore: docs and linting improvements --- .../lib/src/client/retry_queue.dart | 21 ++--- .../lib/src/core/models/message.dart | 85 ++++++++++--------- .../lib/src/message_input/message_input.dart | 70 +++++++-------- .../stream_attachment_picker.dart | 11 ++- .../stream_message_text_field.dart | 59 +++++++------ .../lib/src/message_input_controller.dart | 25 +++--- 6 files changed, 129 insertions(+), 142 deletions(-) diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 7c3157b2..ad140ad6 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -1,18 +1,13 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat/src/client/channel.dart'; import 'package:stream_chat/src/client/retry_policy.dart'; -import 'package:stream_chat/src/core/error/error.dart'; -import 'package:stream_chat/src/core/models/message.dart'; -import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/stream_chat.dart'; -/// The retry queue associated to a channel +/// The retry queue associated to a channel. class RetryQueue { - /// Instantiate a new RetryQueue object + /// Instantiate a new RetryQueue object. RetryQueue({ required this.channel, this.logger, @@ -22,13 +17,13 @@ class RetryQueue { _listenFailedEvents(); } - /// The channel of this queue + /// The channel of this queue. final Channel channel; - /// The client associated with this [channel] + /// The client associated with this [channel]. final StreamChatClient client; - /// The logger associated to this queue + /// The logger associated to this queue. final Logger? logger; late final RetryPolicy _retryPolicy; @@ -68,7 +63,7 @@ class RetryQueue { }).addTo(_compositeSubscription); } - /// Add a list of messages + /// Add a list of messages. void add(List messages) { if (messages.isEmpty) return; if (!_messageQueue.containsAllMessage(messages)) { @@ -180,10 +175,10 @@ class RetryQueue { } } - /// Whether our [_messageQueue] has messages or not + /// Whether our [_messageQueue] has messages or not. bool get hasMessages => _messageQueue.isNotEmpty; - /// Call this method to dispose this object + /// Call this method to dispose this object. void dispose() { _messageQueue.clear(); _compositeSubscription.dispose(); diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 85bf8a96..f1408d45 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -14,7 +14,7 @@ class _PinExpires { const _pinExpires = _PinExpires(); -/// Enum defining the status of a sending message +/// Enum defining the status of a sending message. enum MessageSendingStatus { /// Message is being sent sending, @@ -40,10 +40,10 @@ enum MessageSendingStatus { sent, } -/// The class that contains the information about a message +/// The class that contains the information about a message. @JsonSerializable() class Message extends Equatable { - /// Constructor used for json serialization + /// Constructor used for json serialization. Message({ String? id, this.text, @@ -65,13 +65,13 @@ class Message extends Equatable { this.command, DateTime? createdAt, DateTime? updatedAt, + this.deletedAt, this.user, this.pinned = false, this.pinnedAt, DateTime? pinExpires, this.pinnedBy, this.extraData = const {}, - this.deletedAt, this.status = MessageSendingStatus.sending, this.i18n, }) : id = id ?? const Uuid().v4(), @@ -80,7 +80,7 @@ class Message extends Equatable { _updatedAt = updatedAt, _quotedMessageId = quotedMessageId; - /// Create a new instance from a json + /// Create a new instance from JSON. factory Message.fromJson(Map json) => _$MessageFromJson( Serializer.moveToExtraDataFromRoot(json, topLevelFields), ).copyWith( @@ -91,14 +91,14 @@ class Message extends Equatable { /// the message is added. final String id; - /// The text of this message + /// The text of this message. final String? text; - /// The status of a sending message + /// The status of a sending message. @JsonKey(ignore: true) final MessageSendingStatus status; - /// The message type + /// The message type. @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, @@ -110,15 +110,15 @@ class Message extends Equatable { @JsonKey(includeIfNull: false) final List attachments; - /// The list of user mentioned in the message + /// The list of user mentioned in the message. @JsonKey(toJson: User.toIds) final List mentionedUsers; - /// A map describing the count of number of every reaction + /// A map describing the count of number of every reaction. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final Map? reactionCounts; - /// A map describing the count of score of every reaction + /// A map describing the count of score of every reaction. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final Map? reactionScores; @@ -133,7 +133,7 @@ class Message extends Equatable { /// The ID of the parent message, if the message is a thread reply. final String? parentId; - /// A quoted reply message + /// A quoted reply message. @JsonKey(toJson: Serializer.readOnly) final Message? quotedMessage; @@ -153,10 +153,10 @@ class Message extends Equatable { /// Check if this message needs to show in the channel. final bool? showInChannel; - /// If true the message is silent + /// If true the message is silent. final bool silent; - /// If true the message is shadowed + /// If true the message is shadowed. @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, @@ -169,6 +169,10 @@ class Message extends Equatable { final DateTime? _createdAt; + /// Reserved field indicating when the message was deleted. + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final DateTime? deletedAt; + /// Reserved field indicating when the message was created. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) DateTime get createdAt => _createdAt ?? DateTime.now(); @@ -179,48 +183,45 @@ class Message extends Equatable { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) DateTime get updatedAt => _updatedAt ?? DateTime.now(); - /// User who sent the message + /// User who sent the message. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final User? user; - /// If true the message is pinned + /// If true the message is pinned. final bool pinned; - /// Reserved field indicating when the message was pinned + /// Reserved field indicating when the message was pinned. @JsonKey(toJson: Serializer.readOnly) final DateTime? pinnedAt; - /// Reserved field indicating when the message will expire + /// Reserved field indicating when the message will expire. /// - /// if `null` message has no expiry + /// If `null` message has no expiry. final DateTime? pinExpires; - /// Reserved field indicating who pinned the message + /// Reserved field indicating who pinned the message. @JsonKey(toJson: Serializer.readOnly) final User? pinnedBy; - /// Message custom extraData + /// Message custom extraData. @JsonKey(includeIfNull: false) final Map extraData; - /// True if the message is a system info + /// True if the message is a system info. bool get isSystem => type == 'system'; - /// True if the message has been deleted + /// True if the message has been deleted. bool get isDeleted => type == 'deleted'; - /// True if the message is ephemeral + /// True if the message is ephemeral. bool get isEphemeral => type == 'ephemeral'; - /// Reserved field indicating when the message was deleted. - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime? deletedAt; - /// A Map of translations. @JsonKey(includeIfNull: false) final Map? i18n; /// Known top level fields. + /// /// Useful for [Serializer] methods. static const topLevelFields = [ 'id', @@ -253,7 +254,7 @@ class Message extends Equatable { 'i18n', ]; - /// Serialize to json + /// Serialize to json. Map toJson() => Serializer.moveFromExtraDataToRoot( _$MessageToJson(this), ); @@ -265,6 +266,8 @@ class Message extends Equatable { String? type, List? attachments, List? mentionedUsers, + bool? silent, + bool? shadowed, Map? reactionCounts, Map? reactionScores, List? latestReactions, @@ -275,8 +278,6 @@ class Message extends Equatable { int? replyCount, List? threadParticipants, bool? showInChannel, - bool? shadowed, - bool? silent, String? command, DateTime? createdAt, DateTime? updatedAt, @@ -304,6 +305,8 @@ class Message extends Equatable { type: type ?? this.type, attachments: attachments ?? this.attachments, mentionedUsers: mentionedUsers ?? this.mentionedUsers, + silent: silent ?? this.silent, + shadowed: shadowed ?? this.shadowed, reactionCounts: reactionCounts ?? this.reactionCounts, reactionScores: reactionScores ?? this.reactionScores, latestReactions: latestReactions ?? this.latestReactions, @@ -316,18 +319,16 @@ class Message extends Equatable { showInChannel: showInChannel ?? this.showInChannel, command: command ?? this.command, createdAt: createdAt ?? _createdAt, - silent: silent ?? this.silent, - extraData: extraData ?? this.extraData, - user: user ?? this.user, - shadowed: shadowed ?? this.shadowed, updatedAt: updatedAt ?? _updatedAt, deletedAt: deletedAt ?? this.deletedAt, - status: status ?? this.status, + user: user ?? this.user, pinned: pinned ?? this.pinned, pinnedAt: pinnedAt ?? this.pinnedAt, - pinnedBy: pinnedBy ?? this.pinnedBy, pinExpires: pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?, + pinnedBy: pinnedBy ?? this.pinnedBy, + extraData: extraData ?? this.extraData, + status: status ?? this.status, i18n: i18n ?? this.i18n, ); } @@ -340,6 +341,8 @@ class Message extends Equatable { type: other.type, attachments: other.attachments, mentionedUsers: other.mentionedUsers, + silent: other.silent, + shadowed: other.shadowed, reactionCounts: other.reactionCounts, reactionScores: other.reactionScores, latestReactions: other.latestReactions, @@ -352,17 +355,15 @@ class Message extends Equatable { showInChannel: other.showInChannel, command: other.command, createdAt: other.createdAt, - silent: other.silent, - extraData: other.extraData, - user: other.user, - shadowed: other.shadowed, updatedAt: other.updatedAt, deletedAt: other.deletedAt, - status: other.status, + user: other.user, pinned: other.pinned, pinnedAt: other.pinnedAt, pinExpires: other.pinExpires, pinnedBy: other.pinnedBy, + extraData: other.extraData, + status: other.status, i18n: other.i18n, ); diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 5a3e51e8..26886d87 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -14,16 +13,12 @@ import 'package:stream_chat_flutter/src/commands_overlay.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/src/emoji_overlay.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/multi_overlay.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_mentions_overlay.dart'; import 'package:stream_chat_flutter/src/video_service.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:video_compress/video_compress.dart'; export 'package:video_compress/video_compress.dart' show VideoQuality; @@ -42,13 +37,14 @@ typedef ErrorListener = void Function( /// /// This callback should not throw. /// -/// It exists merely for showing custom error, and should not be used otherwise. +/// It exists merely for showing a custom error, and should not be used +/// otherwise. typedef AttachmentLimitExceedListener = void Function( int limit, String error, ); -/// Builder for attachment thumbnails +/// Builder for attachment thumbnails. typedef AttachmentThumbnailBuilder = Widget Function( BuildContext, Attachment, @@ -77,8 +73,8 @@ typedef ActionButtonBuilder = Widget Function( IconButton defaultActionButton, ); -/// Widget builder for widgets that require may required data from the -/// [MessageInputController] +/// Widget builder for widgets that may require data from the +/// [MessageInputController]. typedef MessageRelatedBuilder = Widget Function( BuildContext context, MessageInputController messageInputController, @@ -91,7 +87,7 @@ typedef AttachmentsPickerBuilder = Widget Function( StreamAttachmentPicker defaultPicker, ); -/// Location for actions on the [MessageInput] +/// Location for actions on the [MessageInput]. enum ActionsLocation { /// Align to left left, @@ -106,7 +102,7 @@ enum ActionsLocation { rightInside, } -/// Default attachments for widget +/// Default attachments for widget. enum DefaultAttachmentTypes { /// Image Attachment image, @@ -118,7 +114,7 @@ enum DefaultAttachmentTypes { file, } -/// Available locations for the sendMessage button relative to the textField +/// Available locations for the `sendMessage` button relative to the textField. enum SendButtonLocation { /// inside the textField inside, @@ -131,17 +127,17 @@ const _kMinMediaPickerSize = 360.0; const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes -/// Inactive state +/// Inactive state: /// /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input_paint.png) /// -/// Focused state +/// Focused state: /// /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2_paint.png) /// -/// Widget used to enter the message and add attachments +/// Widget used to enter a message and add attachments: /// /// ```dart /// class ChannelPage extends StatelessWidget { @@ -176,8 +172,7 @@ const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes /// as the bottom widget. /// /// The widget renders the ui based on the first ancestor of -/// type [StreamChatTheme]. -/// Modify it to change the widget appearance. +/// type [StreamChatTheme]. Modify it to change the widget appearance. class MessageInput extends StatefulWidget { /// Instantiate a new MessageInput const MessageInput({ @@ -218,61 +213,60 @@ class MessageInput extends StatefulWidget { this.shouldKeepFocusAfterMessage, }) : super(key: key); - /// List of options for showing overlays + /// List of options for showing overlays. final List customOverlays; - /// Video quality to use when compressing the videos + /// Video quality to use when compressing the videos. final VideoQuality compressedVideoQuality; - /// Frame rate to use when compressing the videos + /// Frame rate to use when compressing the videos. final int compressedVideoFrameRate; - /// Max attachment size in bytes - /// Defaults to 20 MB - /// do not set it if you're using our default CDN + /// Max attachment size in bytes: + /// - Defaults to 20 MB + /// - Do not set it if you're using our default CDN final int maxAttachmentSize; - /// Function called after sending the message + /// Function called after sending the message. final void Function(Message)? onMessageSent; - /// Function called right before sending the message - /// Use this to transform the message + /// Function called right before sending the message. + /// + /// Use this to transform the message. final FutureOr Function(Message)? preMessageSending; - /// Maximum Height for the TextField to grow before it starts scrolling + /// Maximum Height for the TextField to grow before it starts scrolling. final double maxHeight; - /// The keyboard type assigned to the TextField + /// The keyboard type assigned to the TextField. final TextInputType keyboardType; - /// If true the attachments button will not be displayed + /// If true the attachments button will not be displayed. final bool disableAttachments; - /// Use this property to hide/show the commands button + /// Use this property to hide/show the commands button. final bool showCommandsButton; - /// Hide send as dm checkbox + /// Hide send as dm checkbox. final bool hideSendAsDm; - /// The text controller of the TextField + /// The text controller of the TextField. final MessageInputController? messageInputController; - /// List of action widgets + /// List of action widgets. final List actions; - /// The location of the custom actions + /// The location of the custom actions. final ActionsLocation actionsLocation; - /// Map that defines a thumbnail builder for an attachment type + /// Map that defines a thumbnail builder for an attachment type. final Map? attachmentThumbnailBuilders; - /// The focus node associated to the TextField + /// The focus node associated to the TextField. final FocusNode? focusNode; - /// final Message? quotedMessage; - /// final VoidCallback? onQuotedMessageCleared; /// The location of the send button diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart index 066ed780..88fbab74 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart @@ -21,7 +21,6 @@ typedef CustomAttachmentIconBuilder = Widget Function( bool active, ); -/// class StreamAttachmentPicker extends StatefulWidget { final bool isOpen; final double pickerSize; @@ -32,15 +31,15 @@ class StreamAttachmentPicker extends StatefulWidget { final ValueChanged? onError; final FilePickerCallback onFilePicked; - /// Video quality to use when compressing the videos + /// Video quality to use when compressing the videos. final VideoQuality compressedVideoQuality; - /// Frame rate to use when compressing the videos + /// Frame rate to use when compressing the videos. final int compressedVideoFrameRate; - /// Max attachment size in bytes - /// Defaults to 20 MB - /// do not set it if you're using our default CDN + /// Max attachment size in bytes: + /// - Defaults to 20 MB + /// - Do not set it if you're using our default CDN final int maxAttachmentSize; final List allowedAttachmentTypes; diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index 37b4f683..e5ffde94 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -21,8 +21,8 @@ export 'package:flutter/services.dart' 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. + /// 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 @@ -115,7 +115,7 @@ class StreamMessageTextField extends StatefulWidget { this.selectionHeightStyle = ui.BoxHeightStyle.tight, this.selectionWidthStyle = ui.BoxWidthStyle.tight, this.keyboardAppearance, - this.scrollPadding = const EdgeInsets.all(20.0), + this.scrollPadding = const EdgeInsets.all(20), this.dragStartBehavior = DragStartBehavior.start, this.enableInteractiveSelection = true, this.selectionControls, @@ -127,34 +127,24 @@ class StreamMessageTextField extends StatefulWidget { 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), + }) : assert(obscuringCharacter.length == 1, + '`obscuringCharacter.length` must be 1'), 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 || maxLines > 0, + '`maxLines` needs to be left as null or bigger than 0'), + assert(minLines == null || minLines > 0, + '`minLines` needs to be left as null or bigger than 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.', @@ -164,14 +154,15 @@ class StreamMessageTextField extends StatefulWidget { 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 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.', + '''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 ?? @@ -230,7 +221,8 @@ class StreamMessageTextField extends StatefulWidget { /// 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()]. + /// showing when it is tapped by calling + /// [EditableTextState.requestKeyboard()]. final FocusNode? focusNode; /// The decoration to show around the text field. @@ -330,16 +322,20 @@ class StreamMessageTextField extends StatefulWidget { /// 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. + /// 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. + /// 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. /// - /// 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. @@ -446,7 +442,8 @@ class StreamMessageTextField extends StatefulWidget { /// /// This setting is only honored on iOS devices. /// - /// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness]. + /// If unset, defaults to the brightness of + /// [ThemeData.primaryColorBrightness]. final Brightness? keyboardAppearance; /// {@macro flutter.widgets.editableText.scrollPadding} @@ -490,14 +487,16 @@ class StreamMessageTextField extends StatefulWidget { /// widget. /// /// If [mouseCursor] is a [MaterialStateProperty], - /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: + /// [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. + /// 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" diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 59dfbff7..d0658f57 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -4,7 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat/stream_chat.dart'; -/// A value listenable builder related to a [Message] +/// A value listenable builder related to a [Message]. +/// +/// Pass in a [MessageInputController] as the `valueListenable`. typedef MessageValueListenableBuilder = ValueListenableBuilder; /// A function that returns true if the message is valid and can be sent. @@ -35,7 +37,7 @@ class MessageInputController extends ValueNotifier { validator: validator ?? _defaultValidator, ); - /// Creates a controller for an editable text field from an initial + /// Creates a controller for an editable text field from initial /// [attachments]. factory MessageInputController.fromAttachments( List attachments, { @@ -127,7 +129,7 @@ class MessageInputController extends ValueNotifier { /// Returns the start of the selection of the text field. int get selectionStart => textEditingController.selection.start; - /// Sets the showInChannel flag of the message. + /// Sets the [showInChannel] flag of the message. set showInChannel(bool newValue) { value = value.copyWith(showInChannel: newValue); } @@ -202,14 +204,14 @@ class MessageInputController extends ValueNotifier { mentionedUsers = []; } - /// Set the [value] to empty. + /// Sets the [message], or [value], to empty. /// /// After calling this function, [text], [attachments] and [mentionedUsers] - /// all will be empty. + /// will all be empty. /// /// Calling this will notify all the listeners of this /// [MessageInputController] that they need to update - /// (it calls [notifyListeners]). For this reason, + /// (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() { @@ -217,7 +219,7 @@ class MessageInputController extends ValueNotifier { _textEditingController.clear(); } - /// Set the [value] to the initial [Message] value. + /// Sets the [value] to the initial [Message] value. void reset({bool resetId = true}) { if (resetId) { _initialMessage = _initialMessage.copyWith( @@ -246,16 +248,13 @@ class RestorableMessageInputController extends RestorableChangeNotifier { /// Creates a [RestorableMessageInputController]. /// - /// This constructor treats a null `text` argument as if it were the empty - /// string. + /// This constructor creates a default [Message] when no `message` argument + /// is supplied. 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]. + /// [text] value. factory RestorableMessageInputController.fromText(String? text) => RestorableMessageInputController(message: Message(text: text)); From 5567708fe9645de6e00fcc8b4df8eddc420072ae Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 13 Dec 2021 15:50:33 +0530 Subject: [PATCH 26/58] feat(llc): Add enrichUrl endpoint. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/client.dart | 4 ++ .../lib/src/core/api/general_api.dart | 12 ++++++ .../lib/src/core/api/responses.dart | 40 +++++++++++++++++++ .../lib/src/core/api/responses.g.dart | 15 +++++++ .../test/src/client/client_test.dart | 40 +++++++++++++------ .../test/src/core/api/general_api_test.dart | 39 ++++++++++++++++-- 6 files changed, 133 insertions(+), 17 deletions(-) diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 29e500ce..b2ae3657 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1316,6 +1316,10 @@ class StreamChatClient { }, ); + /// Get OpenGraph data of the given [url]. + Future enrichUrl(String url) => + _chatApi.general.enrichUrl(url); + /// Closes the [_ws] connection and resets the [state] /// If [flushChatPersistence] is true the client deletes all offline /// user's data. diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index fa08a475..9a2e773c 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -96,4 +96,16 @@ class GeneralApi { return QueryMembersResponse.fromJson(response.data); } + + /// Get OpenGraph data of the given [url]. + Future enrichUrl(String url) async { + final response = await _client.get( + '/og', + queryParameters: { + 'url': url, + }, + ); + + return OGAttachmentResponse.fromJson(response.data); + } } diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 5b52b65d..937e0150 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -442,3 +442,43 @@ class ChannelStateResponse extends _BaseResponse { static ChannelStateResponse fromJson(Map json) => _$ChannelStateResponseFromJson(json); } + +/// Model response for [Client.enrichUrl] api call. +@JsonSerializable(createToJson: false) +class OGAttachmentResponse extends _BaseResponse { + /// The URL of the page that was scraped. + late String ogScrapeUrl; + + /// The URL of the asset. + String? assetUrl; + + /// The URL of the author. + String? authorLink; + + /// The name of the author. + String? authorName; + + /// The URL of the image. + String? imageUrl; + + /// The text of the attachment. + String? text; + + /// The URL of the thumbnail. + String? thumbUrl; + + /// The title of the attachment. + String? title; + + /// The URL of the title. + String? titleLink; + + /// The type of the attachment. + /// + /// 'video' | 'audio' | 'image' + String? type; + + /// Create a new instance from a [json]. + static OGAttachmentResponse fromJson(Map json) => + _$OGAttachmentResponseFromJson(json); +} diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 13d7d77b..fbcdd252 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -273,3 +273,18 @@ ChannelStateResponse _$ChannelStateResponseFromJson( ?.map((e) => Read.fromJson(e as Map)) .toList() ?? []; + +OGAttachmentResponse _$OGAttachmentResponseFromJson( + Map json) => + OGAttachmentResponse() + ..duration = json['duration'] as String? + ..ogScrapeUrl = json['og_scrape_url'] as String + ..assetUrl = json['asset_url'] as String? + ..authorLink = json['author_link'] as String? + ..authorName = json['author_name'] as String? + ..imageUrl = json['image_url'] as String? + ..text = json['text'] as String? + ..thumbUrl = json['thumb_url'] as String? + ..title = json['title'] as String? + ..titleLink = json['title_link'] as String? + ..type = json['type'] as String?; diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 36c5919e..22e0edf8 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -1,20 +1,7 @@ import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/client/client.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; -import 'package:stream_chat/src/core/api/requests.dart'; -import 'package:stream_chat/src/core/api/responses.dart'; -import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/http/token.dart'; -import 'package:stream_chat/src/core/models/channel_model.dart'; -import 'package:stream_chat/src/core/models/event.dart'; -import 'package:stream_chat/src/core/models/filter.dart'; -import 'package:stream_chat/src/core/models/message.dart'; -import 'package:stream_chat/src/core/models/own_user.dart'; -import 'package:stream_chat/src/core/models/user.dart'; -import 'package:stream_chat/src/event_type.dart'; -import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:test/scaffolding.dart'; import 'package:test/test.dart'; import '../fakes.dart'; @@ -2314,6 +2301,33 @@ void main() { verifyNoMoreInteractions(api.message); }); + test('`.enrichUrl`', () async { + const url = + 'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example'; + + when(() => api.general.enrichUrl(url)).thenAnswer( + (_) async => OGAttachmentResponse() + ..type = 'image' + ..ogScrapeUrl = url + ..authorName = 'TechYourChance' + ..title = 'Finite State Machine with Unit Tests: Real World Example', + ); + + final res = await client.enrichUrl(url); + + expect(res, isNotNull); + expect(res.type, 'image'); + expect(res.ogScrapeUrl, url); + expect(res.authorName, 'TechYourChance'); + expect( + res.title, + 'Finite State Machine with Unit Tests: Real World Example', + ); + + verify(() => api.general.enrichUrl(url)).called(1); + verifyNoMoreInteractions(api.general); + }); + test( '''setting the `currentUser` should also compute and update the unreadCounts''', () { diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart index 570d7c77..0a27cce4 100644 --- a/packages/stream_chat/test/src/core/api/general_api_test.dart +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -3,10 +3,6 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/general_api.dart'; -import 'package:stream_chat/src/core/api/requests.dart'; -import 'package:stream_chat/src/core/models/channel_model.dart'; -import 'package:stream_chat/src/core/models/event.dart'; -import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; @@ -281,4 +277,39 @@ void main() { verifyNoMoreInteractions(client); }); }); + + test('enrichUrl', () async { + const path = '/og'; + const url = + 'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example'; + + when(() => client.get( + path, + queryParameters: {'url': url}, + )).thenAnswer((_) async => successResponse(path, data: { + 'type': 'image', + 'og_scrape_url': url, + 'author_name': 'TechYourChance', + 'title': 'Finite State Machine with Unit Tests: Real World Example', + })); + + final res = await generalApi.enrichUrl(url); + + expect(res, isNotNull); + expect(res.type, 'image'); + expect(res.ogScrapeUrl, url); + expect(res.authorName, 'TechYourChance'); + expect( + res.title, + 'Finite State Machine with Unit Tests: Real World Example', + ); + + verify( + () => client.get( + path, + queryParameters: {'url': url}, + ), + ).called(1); + verifyNoMoreInteractions(client); + }); } From a5242627f9828e9a901bc3245ebcfeee21dc908e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 13 Dec 2021 15:53:36 +0530 Subject: [PATCH 27/58] chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 940d25c5..f7e02ba4 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +✅ Added + +- Added `client.enrichUrl` endpoint for enriching URLs with metadata. + ## 3.3.1 🐞 Fixed From 527ac4563f10ad9c2e9de8f5dee6d94699703903 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 13 Dec 2021 11:33:46 +0100 Subject: [PATCH 28/58] fix tests --- .../lib/src/core/models/message.dart | 16 +++----- .../test/src/client/channel_test.dart | 37 +++++++++++++++---- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 85bf8a96..82bc7e28 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -76,8 +76,8 @@ class Message extends Equatable { this.i18n, }) : id = id ?? const Uuid().v4(), pinExpires = pinExpires?.toUtc(), - _createdAt = createdAt, - _updatedAt = updatedAt, + createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(), _quotedMessageId = quotedMessageId; /// Create a new instance from a json @@ -167,17 +167,13 @@ class Message extends Equatable { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? command; - final DateTime? _createdAt; - /// Reserved field indicating when the message was created. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - DateTime get createdAt => _createdAt ?? DateTime.now(); - - final DateTime? _updatedAt; + final DateTime createdAt; /// Reserved field indicating when the message was updated last time. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - DateTime get updatedAt => _updatedAt ?? DateTime.now(); + final DateTime updatedAt; /// User who sent the message @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @@ -315,12 +311,12 @@ class Message extends Equatable { threadParticipants: threadParticipants ?? this.threadParticipants, showInChannel: showInChannel ?? this.showInChannel, command: command ?? this.command, - createdAt: createdAt ?? _createdAt, + createdAt: createdAt ?? this.createdAt, silent: silent ?? this.silent, extraData: extraData ?? this.extraData, user: user ?? this.user, shadowed: shadowed ?? this.shadowed, - updatedAt: updatedAt ?? _updatedAt, + updatedAt: updatedAt ?? this.updatedAt, deletedAt: deletedAt ?? this.deletedAt, status: status ?? this.status, pinned: pinned ?? this.pinned, diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index c217431a..60e8dd93 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -244,9 +244,13 @@ void main() { group('`.sendMessage`', () { test('should work fine', () async { - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + ); - final sendMessageResponse = SendMessageResponse()..message = message; + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(status: MessageSendingStatus.sent); when(() => client.sendMessage( any(that: isSameMessageAs(message)), @@ -329,6 +333,7 @@ void main() { .map((it) => it.copyWith(uploadState: const UploadState.success())) .toList(growable: false), + status: MessageSendingStatus.sent, )); expectLater( @@ -455,7 +460,10 @@ void main() { group('`.updateMessage`', () { test('should work fine', () async { - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); final updateMessageResponse = UpdateMessageResponse() ..message = message; @@ -530,6 +538,7 @@ void main() { any(that: isSameMessageAs(message)), )).thenAnswer((_) async => UpdateMessageResponse() ..message = message.copyWith( + status: MessageSendingStatus.sent, attachments: attachments .map((it) => it.copyWith(uploadState: const UploadState.success())) @@ -678,7 +687,7 @@ void main() { [ isSameMessageAs( updateMessageResponse.message.copyWith( - status: MessageSendingStatus.sent, + status: MessageSendingStatus.sending, ), matchText: true, matchSendingStatus: true, @@ -707,7 +716,10 @@ void main() { group('`.deleteMessage`', () { test('should work fine', () async { const messageId = 'test-message-id'; - final message = Message(id: messageId); + final message = Message( + id: messageId, + status: MessageSendingStatus.sent, + ); when(() => client.deleteMessage(messageId)) .thenAnswer((_) async => EmptyResponse()); @@ -1077,7 +1089,10 @@ void main() { group('`.sendReaction`', () { test('should work fine', () async { const type = 'test-reaction-type'; - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); final reaction = Reaction(type: type, messageId: message.id); @@ -1120,7 +1135,10 @@ void main() { 'should restore previous message if `client.sendReaction` throws', () async { const type = 'test-reaction-type'; - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); final reaction = Reaction(type: type, messageId: message.id); @@ -1181,6 +1199,7 @@ void main() { latestReactions: [prevReaction], reactionScores: const {prevType: 1}, reactionCounts: const {prevType: 1}, + status: MessageSendingStatus.sent, ); const type = 'test-reaction-type-2'; @@ -1212,7 +1231,7 @@ void main() { emitsInOrder([ [ isSameMessageAs( - newMessage.copyWith(status: MessageSendingStatus.sent), + newMessage, matchReactions: true, matchSendingStatus: true, ), @@ -1255,6 +1274,7 @@ void main() { latestReactions: [reaction], reactionScores: const {type: 1}, reactionCounts: const {type: 1}, + status: MessageSendingStatus.sent, ); when(() => client.deleteReaction(messageId, type)) @@ -1302,6 +1322,7 @@ void main() { latestReactions: [reaction], reactionScores: const {type: 1}, reactionCounts: const {type: 1}, + status: MessageSendingStatus.sent, ); when(() => client.deleteReaction(messageId, type)) From b372f7b3f8470dc799f67dc02a60231805db04d2 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 13 Dec 2021 11:44:33 +0100 Subject: [PATCH 29/58] revert message.dart --- .../stream_chat/lib/src/core/models/message.dart | 16 ++++++++++------ .../lib/src/message_input/message_input.dart | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 82bc7e28..85bf8a96 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -76,8 +76,8 @@ class Message extends Equatable { this.i18n, }) : id = id ?? const Uuid().v4(), pinExpires = pinExpires?.toUtc(), - createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(), + _createdAt = createdAt, + _updatedAt = updatedAt, _quotedMessageId = quotedMessageId; /// Create a new instance from a json @@ -167,13 +167,17 @@ class Message extends Equatable { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? command; + final DateTime? _createdAt; + /// Reserved field indicating when the message was created. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime createdAt; + DateTime get createdAt => _createdAt ?? DateTime.now(); + + final DateTime? _updatedAt; /// Reserved field indicating when the message was updated last time. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime updatedAt; + DateTime get updatedAt => _updatedAt ?? DateTime.now(); /// User who sent the message @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @@ -311,12 +315,12 @@ class Message extends Equatable { threadParticipants: threadParticipants ?? this.threadParticipants, showInChannel: showInChannel ?? this.showInChannel, command: command ?? this.command, - createdAt: createdAt ?? this.createdAt, + createdAt: createdAt ?? _createdAt, silent: silent ?? this.silent, extraData: extraData ?? this.extraData, user: user ?? this.user, shadowed: shadowed ?? this.shadowed, - updatedAt: updatedAt ?? this.updatedAt, + updatedAt: updatedAt ?? _updatedAt, deletedAt: deletedAt ?? this.deletedAt, status: status ?? this.status, pinned: pinned ?? this.pinned, diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 5a3e51e8..99acb33a 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -1526,7 +1526,7 @@ class MessageInputState extends State { Future sendMessage() async { var message = messageInputController.value; - if (messageInputController.isValid) { + if (!messageInputController.isValid) { return; } From 67f9700b4a3149eab56cfaaec4e4bb0eac8b1949 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 13 Dec 2021 17:17:23 +0100 Subject: [PATCH 30/58] restoration wip --- .../lib/src/message_input/message_input.dart | 204 +++++++++++------- .../stream_attachment_picker.dart | 16 +- .../stream_message_send_button.dart | 33 ++- .../stream_message_text_field.dart | 26 +-- .../lib/src/channels_bloc.dart | 1 - .../lib/src/message_input_controller.dart | 27 +-- .../lib/src/message_list_core.dart | 1 - .../test/src/mapper/message_mapper_test.dart | 1 - .../mapper/pinned_message_mapper_test.dart | 1 - 9 files changed, 171 insertions(+), 139 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index b7eb4cf2..caceba36 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -23,6 +23,9 @@ import 'package:video_compress/video_compress.dart'; export 'package:video_compress/video_compress.dart' show VideoQuality; +/// A function that returns true if the message is valid and can be sent. +typedef MessageValidator = bool Function(Message message); + /// A callback that can be passed to [MessageInput.onError]. /// /// This callback should not throw. @@ -211,6 +214,8 @@ class MessageInput extends StatefulWidget { this.attachmentsPickerBuilder, this.sendButtonBuilder, this.shouldKeepFocusAfterMessage, + this.validator = _defaultValidator, + this.restorationId, }) : super(key: key); /// List of options for showing overlays. @@ -265,8 +270,10 @@ class MessageInput extends StatefulWidget { /// The focus node associated to the TextField. final FocusNode? focusNode; + /// The message that is being quoted. final Message? quotedMessage; + /// Callback invoked when the quoted message is cleared. final VoidCallback? onQuotedMessageCleared; /// The location of the send button @@ -325,6 +332,15 @@ class MessageInput extends StatefulWidget { /// The default behaviour keeps focus until a command is enabled. final bool? shouldKeepFocusAfterMessage; + /// A callback function that validates the message. + final MessageValidator validator; + + /// Restoration ID to save and restore the state of the MessageInput. + final String? restorationId; + + static bool _defaultValidator(Message message) => + message.text?.isNotEmpty == true || message.attachments.isNotEmpty; + @override MessageInputState createState() => MessageInputState(); @@ -341,40 +357,77 @@ class MessageInput extends StatefulWidget { } /// State of [MessageInput] -class MessageInputState extends State { +class MessageInputState extends State + with RestorationMixin { final _imagePicker = ImagePicker(); late final _focusNode = widget.focusNode ?? FocusNode(); bool _inputEnabled = true; - bool get _commandEnabled => messageInputController.value.command != null; + bool get _commandEnabled => _effectiveController.value.command != null; bool _showCommandsOverlay = false; bool _showMentionsOverlay = false; bool _actionsShrunk = false; bool _openFilePickerSection = false; - /// The editing controller passed to the input TextField - late final MessageInputController messageInputController = - widget.messageInputController ?? MessageInputController(); - late StreamChatThemeData _streamChatTheme; late MessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => - messageInputController.value.quotedMessage != null; - - bool get _messageIsPresent => messageInputController.text.trim().isNotEmpty; + _effectiveController.value.quotedMessage != null; bool get _isEditing => - messageInputController.value.status != MessageSendingStatus.sending; + _effectiveController.value.status != MessageSendingStatus.sending; + + RestorableMessageInputController? _controller; + MessageInputController get _effectiveController => + widget.messageInputController ?? _controller!.value; + + void _createLocalController([Message? message]) { + assert(_controller == null, ''); + _controller = RestorableMessageInputController(message: message); + print('_controller?.value: ${_controller?.value}'); + } + + void _registerController() { + assert(_controller != null, ''); + registerForRestoration(_controller!, 'messageInputController'); + } @override void initState() { super.initState(); - messageInputController.textEditingController - .addListener(_onChangedDebounced); + if (widget.messageInputController == null) { + _createLocalController(); + print('_controller?.value: ${_controller?.value}'); + } + _effectiveController.textEditingController.addListener(_onChangedDebounced); _focusNode.addListener(_focusNodeListener); } + @override + void didUpdateWidget(covariant MessageInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.messageInputController == null && + oldWidget.messageInputController != null) { + _createLocalController(oldWidget.messageInputController!.value); + } else if (widget.messageInputController != null && + oldWidget.messageInputController == 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 _focusNodeListener() { if (_focusNode.hasFocus) { _openFilePickerSection = false; @@ -414,7 +467,7 @@ class MessageInputState extends State { @override Widget build(BuildContext context) { Widget child = MessageValueListenableBuilder( - valueListenable: messageInputController, + valueListenable: _effectiveController, builder: (context, value, _) => DecoratedBox( decoration: BoxDecoration( color: _messageInputTheme.inputBackgroundColor, @@ -462,7 +515,7 @@ class MessageInputState extends State { padding: const EdgeInsets.symmetric(vertical: 8), child: _buildTextField(context), ), - if (messageInputController.value.parentId != null && + if (_effectiveController.value.parentId != null && !widget.hideSendAsDm) Padding( padding: const EdgeInsets.only( @@ -496,12 +549,12 @@ class MessageInputState extends State { ), OverlayOptions( visible: _focusNode.hasFocus && - messageInputController.text.isNotEmpty && - messageInputController.baseOffset > 0 && - messageInputController.text + _effectiveController.text.isNotEmpty && + _effectiveController.baseOffset > 0 && + _effectiveController.text .substring( 0, - messageInputController.baseOffset, + _effectiveController.baseOffset, ) .contains(':'), widget: _buildEmojiOverlay(), @@ -537,7 +590,7 @@ class MessageInputState extends State { height: 16, width: 16, foregroundDecoration: BoxDecoration( - border: messageInputController.showInChannel + border: _effectiveController.showInChannel ? null : Border.all( color: _streamChatTheme.colorTheme.textHighEmphasis @@ -549,18 +602,18 @@ class MessageInputState extends State { child: Center( child: Material( borderRadius: BorderRadius.circular(3), - color: messageInputController.showInChannel + color: _effectiveController.showInChannel ? _streamChatTheme.colorTheme.accentPrimary : _streamChatTheme.colorTheme.barsBg, child: InkWell( onTap: () { - messageInputController.showInChannel = - !messageInputController.showInChannel; + _effectiveController.showInChannel = + !_effectiveController.showInChannel; }, child: AnimatedCrossFade( duration: const Duration(milliseconds: 300), reverseDuration: const Duration(milliseconds: 300), - crossFadeState: messageInputController.showInChannel + crossFadeState: _effectiveController.showInChannel ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: StreamSvgIcon.check( @@ -591,13 +644,13 @@ class MessageInputState extends State { Widget _buildSendButton(BuildContext context) { if (widget.sendButtonBuilder != null) { - return widget.sendButtonBuilder!(context, messageInputController); + return widget.sendButtonBuilder!(context, _effectiveController); } return StreamMessageSendButton( onSendMessage: sendMessage, timeOut: _timeOut, - isIdle: !_messageIsPresent && messageInputController.attachments.isEmpty, + isIdle: !widget.validator(_effectiveController.message), isEditEnabled: _isEditing, idleSendButton: widget.idleSendButton, activeSendButton: widget.activeSendButton, @@ -696,7 +749,7 @@ class MessageInputState extends State { maxLines: null, onSubmitted: (_) => sendMessage(), keyboardType: widget.keyboardType, - controller: messageInputController, + controller: _effectiveController, focusNode: _focusNode, style: _messageInputTheme.inputTextStyle, autofocus: widget.autofocus, @@ -768,7 +821,7 @@ class MessageInputState extends State { size: 16, ), Text( - messageInputController.value.command!.toUpperCase(), + _effectiveController.value.command!.toUpperCase(), style: _streamChatTheme.textTheme.footnoteBold.copyWith( color: Colors.white, @@ -802,7 +855,7 @@ class MessageInputState extends State { height: 24, width: 24, ), - onPressed: messageInputController.clear, + onPressed: _effectiveController.clear, ), ), if (!_commandEnabled && @@ -817,14 +870,14 @@ class MessageInputState extends State { late final _onChangedDebounced = debounce( () { - var value = messageInputController.text; + var value = _effectiveController.text; if (!mounted) return; value = value.trim(); final channel = StreamChannel.of(context).channel; if (value.isNotEmpty) { channel - .keyStroke(messageInputController.value.parentId) + .keyStroke(_effectiveController.value.parentId) // ignore: no-empty-block .catchError((e) {}); } @@ -846,10 +899,10 @@ class MessageInputState extends State { ); String _getHint(BuildContext context) { - if (_commandEnabled && messageInputController.value.command == 'giphy') { + if (_commandEnabled && _effectiveController.value.command == 'giphy') { return context.translations.searchGifLabel; } - if (messageInputController.attachments.isNotEmpty) { + if (_effectiveController.attachments.isNotEmpty) { return context.translations.addACommentOrSendLabel; } if (_timeOut != 0) { @@ -861,16 +914,16 @@ class MessageInputState extends State { void _checkEmoji(String s, BuildContext context) { if (s.isNotEmpty && - messageInputController.baseOffset > 0 && - messageInputController.text + _effectiveController.baseOffset > 0 && + _effectiveController.text .substring( 0, - messageInputController.baseOffset, + _effectiveController.baseOffset, ) .contains(':')) { - final textToSelection = messageInputController.text.substring( + final textToSelection = _effectiveController.text.substring( 0, - messageInputController.selectionStart, + _effectiveController.selectionStart, ); final splits = textToSelection.split(':'); final query = splits[splits.length - 2].toLowerCase(); @@ -884,11 +937,11 @@ class MessageInputState extends State { void _checkMentions(String s, BuildContext context) { if (s.isNotEmpty && - messageInputController.baseOffset > 0 && - messageInputController.text + _effectiveController.baseOffset > 0 && + _effectiveController.text .substring( 0, - messageInputController.baseOffset, + _effectiveController.baseOffset, ) .split(' ') .last @@ -925,7 +978,7 @@ class MessageInputState extends State { } Widget _buildCommandsOverlayEntry() { - final text = messageInputController.text.trimLeft(); + final text = _effectiveController.text.trimLeft(); final renderObject = context.findRenderObject() as RenderBox?; if (renderObject == null) { @@ -941,7 +994,7 @@ class MessageInputState extends State { Widget _buildFilePickerSection() { final picker = StreamAttachmentPicker( - messageInputController: messageInputController, + messageInputController: _effectiveController, onFilePicked: pickFile, isOpen: _openFilePickerSection, pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0, @@ -950,18 +1003,13 @@ class MessageInputState extends State { maxAttachmentSize: widget.maxAttachmentSize, compressedVideoQuality: widget.compressedVideoQuality, compressedVideoFrameRate: widget.compressedVideoFrameRate, - onChangeInputState: (val) { - setState(() { - _inputEnabled = val; - }); - }, onError: _showErrorAlert, ); if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) { return widget.attachmentsPickerBuilder!( context, - messageInputController, + _effectiveController, picker, ); } @@ -971,14 +1019,14 @@ class MessageInputState extends State { Widget _buildMentionsOverlayEntry() { final channel = StreamChannel.of(context).channel; - if (messageInputController.selectionStart < 0 || channel.state == null) { + if (_effectiveController.selectionStart < 0 || channel.state == null) { return const Offstage(); } - final splits = messageInputController.text + final splits = _effectiveController.text .substring( 0, - messageInputController.selectionStart, + _effectiveController.selectionStart, ) .split('@'); final query = splits.last.toLowerCase(); @@ -1007,13 +1055,13 @@ class MessageInputState extends State { size: Size(renderObject.size.width - 16, 400), mentionsTileBuilder: tileBuilder, onMentionUserTap: (user) { - messageInputController.addMentionedUser(user); + _effectiveController.addMentionedUser(user); splits[splits.length - 1] = user.name; final rejoin = splits.join('@'); - messageInputController.text = rejoin + - messageInputController.text.substring( - messageInputController.selectionStart, + _effectiveController.text = rejoin + + _effectiveController.text.substring( + _effectiveController.selectionStart, ); _onChangedDebounced.cancel(); @@ -1023,14 +1071,14 @@ class MessageInputState extends State { } Widget _buildEmojiOverlay() { - if (messageInputController.baseOffset < 0) { + if (_effectiveController.baseOffset < 0) { return const Offstage(); } - final splits = messageInputController.text + final splits = _effectiveController.text .substring( 0, - messageInputController.baseOffset, + _effectiveController.baseOffset, ) .split(':'); @@ -1050,14 +1098,14 @@ class MessageInputState extends State { void _chooseEmoji(List splits, Emoji emoji) { final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; - messageInputController.text = rejoin + - messageInputController.text.substring( - messageInputController.selectionStart, + _effectiveController.text = rejoin + + _effectiveController.text.substring( + _effectiveController.selectionStart, ); } void _setCommand(Command c) { - messageInputController + _effectiveController ..clear() ..command = c; setState(() { @@ -1079,11 +1127,11 @@ class MessageInputState extends State { } Widget _buildAttachments() { - if (messageInputController.attachments.isEmpty) return const Offstage(); - final fileAttachments = messageInputController.attachments + if (_effectiveController.attachments.isEmpty) return const Offstage(); + final fileAttachments = _effectiveController.attachments .where((it) => it.type == 'file') .toList(growable: false); - final remainingAttachments = messageInputController.attachments + final remainingAttachments = _effectiveController.attachments .where((it) => it.type != 'file') .toList(growable: false); return Column( @@ -1168,9 +1216,8 @@ class MessageInputState extends State { focusElevation: 0, hoverElevation: 0, onPressed: () { - messageInputController.value = - messageInputController.value.copyWith( - attachments: messageInputController.attachments + _effectiveController.value = _effectiveController.value.copyWith( + attachments: _effectiveController.attachments .where((it) => it.id != attachment.id) .toList(), ); @@ -1252,7 +1299,7 @@ class MessageInputState extends State { } Widget _buildCommandButton(BuildContext context) { - final s = messageInputController.text.trim(); + final s = _effectiveController.text.trim(); final defaultButton = IconButton( icon: StreamSvgIcon.lightning( color: s.isNotEmpty @@ -1386,8 +1433,7 @@ class MessageInputState extends State { /// Adds an attachment to the [messageInputController.attachments] map void _addAttachments(Iterable attachments) { final limit = widget.attachmentLimit; - final length = - messageInputController.attachments.length + attachments.length; + final length = _effectiveController.attachments.length + attachments.length; if (length > limit) { final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; if (onAttachmentLimitExceed != null) { @@ -1401,7 +1447,7 @@ class MessageInputState extends State { ); } for (final attachment in attachments) { - messageInputController.addAttachment(attachment); + _effectiveController.addAttachment(attachment); } } @@ -1518,17 +1564,13 @@ class MessageInputState extends State { /// Sends the current message Future sendMessage() async { - var message = messageInputController.value; - - if (!messageInputController.isValid) { - return; - } + var message = _effectiveController.value; var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; shouldKeepFocus ??= !_commandEnabled; - messageInputController.reset(); + _effectiveController.reset(); widget.onQuotedMessageCleared?.call(); if (widget.preMessageSending != null) { @@ -1557,7 +1599,7 @@ class MessageInputState extends State { final resp = await sendingFuture; if (resp.message?.type == 'error') { - messageInputController.value = message; + _effectiveController.value = message; } _startSlowMode(); widget.onMessageSent?.call(resp.message); @@ -1638,9 +1680,9 @@ class MessageInputState extends State { @override void dispose() { - messageInputController.textEditingController + _effectiveController.textEditingController .removeListener(_onChangedDebounced); - messageInputController.dispose(); + _controller?.dispose(); _focusNode.removeListener(_focusNodeListener); _stopSlowMode(); _onChangedDebounced.cancel(); diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart index 88fbab74..7f6a6f6e 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart @@ -21,13 +21,23 @@ typedef CustomAttachmentIconBuilder = Widget Function( bool active, ); +/// A widget that allows to pick an attachment. class StreamAttachmentPicker extends StatefulWidget { + /// True if the picker is open. final bool isOpen; + + /// The picker size in height. final double pickerSize; + + /// The [MessageInputController] linked to this picker. final MessageInputController messageInputController; + + /// The limit of attachments that can be picked. final int attachmentLimit; + + /// The callback for when the attachment limit is exceeded. final AttachmentLimitExceedListener? onAttachmentLimitExceeded; - final ValueChanged? onChangeInputState; + final ValueChanged? onError; final FilePickerCallback onFilePicked; @@ -42,8 +52,10 @@ class StreamAttachmentPicker extends StatefulWidget { /// - Do not set it if you're using our default CDN final int maxAttachmentSize; + /// The list of attachment types that can be picked. final List allowedAttachmentTypes; + /// The list of custom attachment types that can be picked. final List customAttachmentTypes; const StreamAttachmentPicker({ @@ -57,7 +69,6 @@ class StreamAttachmentPicker extends StatefulWidget { this.maxAttachmentSize = 20971520, this.compressedVideoQuality = VideoQuality.DefaultQuality, this.compressedVideoFrameRate = 30, - this.onChangeInputState, this.onError, this.allowedAttachmentTypes = const [ DefaultAttachmentTypes.image, @@ -98,7 +109,6 @@ class StreamAttachmentPicker extends StatefulWidget { compressedVideoQuality ?? this.compressedVideoQuality, compressedVideoFrameRate: compressedVideoFrameRate ?? this.compressedVideoFrameRate, - onChangeInputState: onChangeInputState ?? this.onChangeInputState, onError: onError ?? this.onError, allowedAttachmentTypes: allowedAttachmentTypes ?? this.allowedAttachmentTypes, diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart index ec03a58c..9916f6f7 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart @@ -1,15 +1,11 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// A widget that displays a sending button. class StreamMessageSendButton extends StatelessWidget { - final int timeOut; - final bool isIdle; - final bool isCommandEnabled; - final bool isEditEnabled; - final Widget? idleSendButton; - final Widget? activeSendButton; - final VoidCallback onSendMessage; - + /// Returns a [StreamMessageSendButton] with the given [timeOut], [isIdle], + /// [isCommandEnabled], [isEditEnabled], [idleSendButton], [activeSendButton], + /// [onSendMessage]. const StreamMessageSendButton({ Key? key, this.timeOut = 0, @@ -21,6 +17,27 @@ class StreamMessageSendButton extends StatelessWidget { required this.onSendMessage, }) : super(key: key); + /// Time out related to slow mode. + final int timeOut; + + /// If true the button will be disabled. + final bool isIdle; + + /// True if a command is being sent. + final bool isCommandEnabled; + + /// True if in editing mode. + final bool isEditEnabled; + + /// The widget to display when the button is disabled. + final Widget? idleSendButton; + + /// The widget to display when the button is enabled. + final Widget? activeSendButton; + + /// The callback to call when the button is pressed. + final VoidCallback onSendMessage; + @override Widget build(BuildContext context) { final _streamChatTheme = StreamChatTheme.of(context); diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index e5ffde94..af2548a4 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -18,6 +18,7 @@ export 'package:flutter/services.dart' SmartQuotesType, SmartDashesType; +/// A widget the wraps the [TextField] and adds some StreamChat specifics. class StreamMessageTextField extends StatefulWidget { /// Creates a Material Design text field. /// @@ -47,10 +48,6 @@ class StreamMessageTextField extends StatefulWidget { /// 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. /// @@ -60,7 +57,7 @@ class StreamMessageTextField extends StatefulWidget { /// must not be null. /// /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect], - /// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength], + /// [scrollPadding], [maxLines], [maxLength], /// [selectionHeightStyle], [selectionWidthStyle], [enableSuggestions], and /// [enableIMEPersonalizedLearning] arguments must not be null. /// @@ -151,9 +148,11 @@ class StreamMessageTextField extends StatefulWidget { ), assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'), - assert(maxLength == null || - maxLength == TextField.noMaxLength || - maxLength > 0), + assert( + maxLength == null || + maxLength == TextField.noMaxLength || + maxLength > 0, + '`maxLength` needs to be null or a positive integer'), // Assert the following instead of setting it directly to avoid // surprising the user by silently changing the value they set. @@ -343,12 +342,6 @@ class StreamMessageTextField extends StatefulWidget { /// 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; @@ -611,10 +604,6 @@ class StreamMessageTextField extends StatefulWidget { properties.add( DiagnosticsProperty('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, defaultValue: null)); @@ -742,7 +731,6 @@ class _StreamMessageTextFieldState extends State minLines: widget.minLines, expands: widget.expands, maxLength: widget.maxLength, - maxLengthEnforced: widget.maxLengthEnforced, maxLengthEnforcement: widget.maxLengthEnforcement, onEditingComplete: widget.onEditingComplete, onSubmitted: widget.onSubmitted, diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 0e8d48bc..6514d66b 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 7aad03e6..2b718939 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -9,9 +9,6 @@ import 'package:stream_chat/stream_chat.dart'; /// Pass in a [MessageInputController] as the `valueListenable`. typedef MessageValueListenableBuilder = ValueListenableBuilder; -/// A function that returns true if the message is valid and can be sent. -typedef MessageValidator = bool Function(Message message); - /// Controller for storing and mutating a [Message] value. class MessageInputController extends ValueNotifier { /// Creates a controller for an editable text field. @@ -20,37 +17,28 @@ class MessageInputController extends ValueNotifier { /// message. factory MessageInputController({ Message? message, - MessageValidator? validator, }) => MessageInputController._( initialMessage: message ?? Message(), - validator: validator ?? _defaultValidator, ); /// Creates a controller for an editable text field from an initial [text]. - factory MessageInputController.fromText( - String? text, { - MessageValidator? validator, - }) => + factory MessageInputController.fromText(String? text) => MessageInputController._( initialMessage: Message(text: text), - validator: validator ?? _defaultValidator, ); /// Creates a controller for an editable text field from initial /// [attachments]. factory MessageInputController.fromAttachments( - List attachments, { - MessageValidator? validator, - }) => + List attachments, + ) => MessageInputController._( initialMessage: Message(attachments: attachments), - validator: validator ?? _defaultValidator, ); MessageInputController._({ required Message initialMessage, - this.validator = _defaultValidator, }) : _textEditingController = TextEditingController(text: initialMessage.text), _initialMessage = initialMessage, @@ -58,15 +46,6 @@ class MessageInputController extends ValueNotifier { addListener(_textEditingSyncer); } - /// A callback function that validates the message. - final MessageValidator validator; - - /// Checks if the message is valid. - bool get isValid => validator(value); - - static bool _defaultValidator(Message message) => - message.text?.isNotEmpty == true || message.attachments.isNotEmpty; - void _textEditingSyncer() { final cleanText = value.command == null ? value.text diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index 74e40d7f..a28d6d73 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter_core/src/better_stream_builder.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index d2ea8b93..8a06f02d 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -157,7 +157,6 @@ void main() { (prev, curr) => prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), ), - status: MessageSendingStatus.sending, updatedAt: DateTime.now(), extraData: const {'extra_test_data': 'extraData'}, user: user, diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index 9c2378fd..9c4d99a2 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -147,7 +147,6 @@ void main() { (prev, curr) => prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), ), - status: MessageSendingStatus.sending, updatedAt: DateTime.now(), extraData: const {'extra_test_data': 'extraData'}, user: user, From 9f8c5114959d24a4c9b03feceb562253d20c920c Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 13 Dec 2021 23:14:00 +0530 Subject: [PATCH 31/58] fix: registration --- .../lib/src/message_input/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index caceba36..b9aa00eb 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -385,7 +385,7 @@ class MessageInputState extends State void _createLocalController([Message? message]) { assert(_controller == null, ''); _controller = RestorableMessageInputController(message: message); - print('_controller?.value: ${_controller?.value}'); + _registerController(); } void _registerController() { From a49a0ed7c943c31c8797019c14a75b01cccc2d6f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 13 Dec 2021 23:17:24 +0530 Subject: [PATCH 32/58] removed print --- .../stream_chat_flutter/lib/src/message_input/message_input.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index b9aa00eb..2fc2795a 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -398,7 +398,6 @@ class MessageInputState extends State super.initState(); if (widget.messageInputController == null) { _createLocalController(); - print('_controller?.value: ${_controller?.value}'); } _effectiveController.textEditingController.addListener(_onChangedDebounced); _focusNode.addListener(_focusNodeListener); From ce20c247d7a74e87a2d6133224e16333dab2af38 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 14 Dec 2021 15:15:02 +0100 Subject: [PATCH 33/58] fix emoji overlay --- .../lib/src/message_input/message_input.dart | 209 +++++++++--------- 1 file changed, 108 insertions(+), 101 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 2fc2795a..9787b39e 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -464,109 +464,109 @@ class MessageInputState extends State void _stopSlowMode() => _slowModeTimer?.cancel(); @override - Widget build(BuildContext context) { - Widget child = MessageValueListenableBuilder( - valueListenable: _effectiveController, - builder: (context, value, _) => 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, - ), - ), - 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 (_effectiveController.value.parentId != null && - !widget.hideSendAsDm) - Padding( - padding: const EdgeInsets.only( - right: 12, - left: 12, - bottom: 12, - ), - child: _buildDmCheckbox(), - ), - _buildFilePickerSection(), - ], + Widget build(BuildContext context) => MessageValueListenableBuilder( + valueListenable: _effectiveController, + builder: (context, value, _) { + Widget child = DecoratedBox( + decoration: BoxDecoration( + color: _messageInputTheme.inputBackgroundColor, ), - ), - ), - ), - ); - if (_isEditing) { - child = Material( - elevation: 8, - child: child, + 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, + ), + ), + 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 (_effectiveController.value.parentId != null && + !widget.hideSendAsDm) + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + bottom: 12, + ), + child: _buildDmCheckbox(), + ), + _buildFilePickerSection(), + ], + ), + ), + ), + ); + if (_isEditing) { + child = Material( + elevation: 8, + child: child, + ); + } + return MultiOverlay( + childAnchor: Alignment.topCenter, + overlayAnchor: Alignment.bottomCenter, + overlayOptions: [ + OverlayOptions( + visible: _showCommandsOverlay, + widget: _buildCommandsOverlayEntry(), + ), + OverlayOptions( + visible: _focusNode.hasFocus && + _effectiveController.text.isNotEmpty && + _effectiveController.baseOffset > 0 && + _effectiveController.text + .substring( + 0, + _effectiveController.baseOffset, + ) + .contains(':'), + widget: _buildEmojiOverlay(), + ), + OverlayOptions( + visible: _showMentionsOverlay, + widget: _buildMentionsOverlayEntry(), + ), + ...widget.customOverlays, + ], + child: child, + ); + }, ); - } - - return MultiOverlay( - childAnchor: Alignment.topCenter, - overlayAnchor: Alignment.bottomCenter, - overlayOptions: [ - OverlayOptions( - visible: _showCommandsOverlay, - widget: _buildCommandsOverlayEntry(), - ), - OverlayOptions( - visible: _focusNode.hasFocus && - _effectiveController.text.isNotEmpty && - _effectiveController.baseOffset > 0 && - _effectiveController.text - .substring( - 0, - _effectiveController.baseOffset, - ) - .contains(':'), - widget: _buildEmojiOverlay(), - ), - OverlayOptions( - visible: _showMentionsOverlay, - widget: _buildMentionsOverlayEntry(), - ), - ...widget.customOverlays, - ], - child: child, - ); - } Flex _buildTextField(BuildContext context) => Flex( direction: Axis.horizontal, @@ -646,6 +646,9 @@ class MessageInputState extends State return widget.sendButtonBuilder!(context, _effectiveController); } + print( + 'widget.validator(_effectiveController.message): ${widget.validator(_effectiveController.message)}'); + return StreamMessageSendButton( onSendMessage: sendMessage, timeOut: _timeOut, @@ -711,6 +714,7 @@ class MessageInputState extends State } Expanded _buildTextInput(BuildContext context) { + print('build text input'); final margin = (widget.sendButtonLocation == SendButtonLocation.inside ? const EdgeInsets.only(right: 8) : EdgeInsets.zero) + @@ -869,6 +873,7 @@ class MessageInputState extends State late final _onChangedDebounced = debounce( () { + print('onchangeddebounce'); var value = _effectiveController.text; if (!mounted) return; value = value.trim(); @@ -888,6 +893,7 @@ class MessageInputState extends State setState(() { _actionsShrunk = value.isNotEmpty && actionsLength > 1; }); + print('CHECK COMMANDS 00'); _checkCommands(value, context); _checkMentions(value, context); @@ -959,6 +965,7 @@ class MessageInputState extends State void _checkCommands(String s, BuildContext context) { if (s.startsWith('/')) { + print('CHECK COMMANDS'); final allCommands = StreamChannel.of(context).channel.config?.commands; final command = allCommands?.firstWhereOrNull((it) => it.name == s.substring(1)); From ec28d00e81f0f0d411cf4bedbff4b850404a0625 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 15 Dec 2021 10:51:28 +0100 Subject: [PATCH 34/58] fix message input quoted message --- .../lib/src/core/models/message.dart | 45 ++++++++++++++----- .../lib/src/message_input/message_input.dart | 44 +++++++++--------- .../stream_message_text_field.dart | 2 +- .../lib/src/message_input_controller.dart | 21 ++++++++- .../test/message_input_controller_test.dart | 24 ++-------- 5 files changed, 81 insertions(+), 55 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index f1408d45..a7d21c03 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -8,11 +8,11 @@ import 'package:uuid/uuid.dart'; part 'message.g.dart'; -class _PinExpires { - const _PinExpires(); +class _NullConst { + const _NullConst(); } -const _pinExpires = _PinExpires(); +const _nullConst = _NullConst(); /// Enum defining the status of a sending message. enum MessageSendingStatus { @@ -273,8 +273,8 @@ class Message extends Equatable { List? latestReactions, List? ownReactions, String? parentId, - Message? quotedMessage, - String? quotedMessageId, + Object? quotedMessage = _nullConst, + Object? quotedMessageId = _nullConst, int? replyCount, List? threadParticipants, bool? showInChannel, @@ -285,7 +285,7 @@ class Message extends Equatable { User? user, bool? pinned, DateTime? pinnedAt, - Object? pinExpires = _pinExpires, + Object? pinExpires = _nullConst, User? pinnedBy, Map? extraData, MessageSendingStatus? status, @@ -294,11 +294,32 @@ class Message extends Equatable { assert(() { if (pinExpires is! DateTime && pinExpires != null && - pinExpires is! _PinExpires) { + pinExpires is! _NullConst) { throw ArgumentError('`pinExpires` can only be set as DateTime or null'); } return true; }(), 'Validate type for pinExpires'); + + assert(() { + if (quotedMessage is! Message && + quotedMessage != null && + quotedMessage is! _NullConst) { + throw ArgumentError( + '`quotedMessage` can only be set as Message or null'); + } + return true; + }(), 'Validate type for quotedMessage'); + + assert(() { + if (quotedMessageId is! String && + quotedMessageId != null && + quotedMessageId is! _NullConst) { + throw ArgumentError( + '`quotedMessage` can only be set as String or null'); + } + return true; + }(), 'Validate type for quotedMessage'); + return Message( id: id ?? this.id, text: text ?? this.text, @@ -312,8 +333,12 @@ class Message extends Equatable { latestReactions: latestReactions ?? this.latestReactions, ownReactions: ownReactions ?? this.ownReactions, parentId: parentId ?? this.parentId, - quotedMessage: quotedMessage ?? this.quotedMessage, - quotedMessageId: quotedMessageId ?? _quotedMessageId, + quotedMessage: quotedMessage == _nullConst + ? this.quotedMessage + : quotedMessage as Message?, + quotedMessageId: quotedMessageId == _nullConst + ? _quotedMessageId + : quotedMessageId as String?, replyCount: replyCount ?? this.replyCount, threadParticipants: threadParticipants ?? this.threadParticipants, showInChannel: showInChannel ?? this.showInChannel, @@ -325,7 +350,7 @@ class Message extends Equatable { pinned: pinned ?? this.pinned, pinnedAt: pinnedAt ?? this.pinnedAt, pinExpires: - pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?, + pinExpires == _nullConst ? this.pinExpires : pinExpires as DateTime?, pinnedBy: pinnedBy ?? this.pinnedBy, extraData: extraData ?? this.extraData, status: status ?? this.status, diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 9787b39e..2ba5c038 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -190,8 +190,6 @@ class MessageInput extends StatefulWidget { this.actionsLocation = ActionsLocation.left, this.attachmentThumbnailBuilders, this.focusNode, - this.quotedMessage, - this.onQuotedMessageCleared, this.sendButtonLocation = SendButtonLocation.outside, this.autofocus = false, this.hideSendAsDm = false, @@ -270,12 +268,6 @@ class MessageInput extends StatefulWidget { /// The focus node associated to the TextField. final FocusNode? focusNode; - /// The message that is being quoted. - final Message? quotedMessage; - - /// Callback invoked when the quoted message is cleared. - final VoidCallback? onQuotedMessageCleared; - /// The location of the send button final SendButtonLocation sendButtonLocation; @@ -385,12 +377,19 @@ class MessageInputState extends State void _createLocalController([Message? message]) { assert(_controller == null, ''); _controller = RestorableMessageInputController(message: message); - _registerController(); } void _registerController() { assert(_controller != null, ''); - registerForRestoration(_controller!, 'messageInputController'); + + registerForRestoration( + _controller!, + widget.restorationId ?? 'messageInputController', + ); + _effectiveController.textEditingController + .removeListener(_onChangedDebounced); + _effectiveController.textEditingController.addListener(_onChangedDebounced); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); } @override @@ -398,8 +397,13 @@ class MessageInputState extends State super.initState(); if (widget.messageInputController == null) { _createLocalController(); + } else { + _effectiveController.textEditingController + .removeListener(_onChangedDebounced); + _effectiveController.textEditingController + .addListener(_onChangedDebounced); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); } - _effectiveController.textEditingController.addListener(_onChangedDebounced); _focusNode.addListener(_focusNodeListener); } @@ -506,7 +510,10 @@ class MessageInputState extends State IconButton( visualDensity: VisualDensity.compact, icon: StreamSvgIcon.closeSmall(), - onPressed: widget.onQuotedMessageCleared, + onPressed: () { + _effectiveController.clearQuotedMessage(); + _focusNode.unfocus(); + }, ), ], ), @@ -646,9 +653,6 @@ class MessageInputState extends State return widget.sendButtonBuilder!(context, _effectiveController); } - print( - 'widget.validator(_effectiveController.message): ${widget.validator(_effectiveController.message)}'); - return StreamMessageSendButton( onSendMessage: sendMessage, timeOut: _timeOut, @@ -714,7 +718,6 @@ class MessageInputState extends State } Expanded _buildTextInput(BuildContext context) { - print('build text input'); final margin = (widget.sendButtonLocation == SendButtonLocation.inside ? const EdgeInsets.only(right: 8) : EdgeInsets.zero) + @@ -873,7 +876,6 @@ class MessageInputState extends State late final _onChangedDebounced = debounce( () { - print('onchangeddebounce'); var value = _effectiveController.text; if (!mounted) return; value = value.trim(); @@ -893,7 +895,6 @@ class MessageInputState extends State setState(() { _actionsShrunk = value.isNotEmpty && actionsLength > 1; }); - print('CHECK COMMANDS 00'); _checkCommands(value, context); _checkMentions(value, context); @@ -965,7 +966,6 @@ class MessageInputState extends State void _checkCommands(String s, BuildContext context) { if (s.startsWith('/')) { - print('CHECK COMMANDS'); final allCommands = StreamChannel.of(context).channel.config?.commands; final command = allCommands?.firstWhereOrNull((it) => it.name == s.substring(1)); @@ -1121,12 +1121,12 @@ class MessageInputState extends State Widget _buildReplyToMessage() { if (!_hasQuotedMessage) return const Offstage(); - final containsUrl = widget.quotedMessage!.attachments + final containsUrl = _effectiveController.value.quotedMessage!.attachments .any((element) => element.titleLink != null); return QuotedMessageWidget( reverse: true, showBorder: !containsUrl, - message: widget.quotedMessage!, + message: _effectiveController.value.quotedMessage!, messageTheme: _streamChatTheme.otherMessageTheme, padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), ); @@ -1577,7 +1577,6 @@ class MessageInputState extends State shouldKeepFocus ??= !_commandEnabled; _effectiveController.reset(); - widget.onQuotedMessageCleared?.call(); if (widget.preMessageSending != null) { message = await widget.preMessageSending!(message); @@ -1699,7 +1698,6 @@ class MessageInputState extends State void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); _messageInputTheme = MessageInputTheme.of(context); - if (!_isEditing && _timeOut <= 0) _startSlowMode(); super.didChangeDependencies(); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index af2548a4..e7d765d9 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -697,7 +697,7 @@ class _StreamMessageTextFieldState extends State void _registerController() { assert(_controller != null, ''); - registerForRestoration(_controller!, 'controller'); + registerForRestoration(_controller!, restorationId ?? 'controller'); } @override diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 2b718939..29bd2db2 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -40,7 +40,10 @@ class MessageInputController extends ValueNotifier { MessageInputController._({ required Message initialMessage, }) : _textEditingController = - TextEditingController(text: initialMessage.text), + TextEditingController.fromValue(TextEditingValue( + text: initialMessage.text ?? '', + composing: TextRange.collapsed(initialMessage.text?.length ?? 0), + )), _initialMessage = initialMessage, super(initialMessage) { addListener(_textEditingSyncer); @@ -83,6 +86,22 @@ class MessageInputController extends ValueNotifier { value = message; } + /// Sets the message that's being quoted. + set quotedMessage(Message message) { + value = value.copyWith( + quotedMessage: message, + quotedMessageId: message.id, + ); + } + + /// Clears the quoted message. + void clearQuotedMessage() { + value = value.copyWith( + quotedMessageId: null, + quotedMessage: null, + ); + } + /// Sets a command for the message. set command(Command command) { value = value.copyWith( diff --git a/packages/stream_chat_flutter_core/test/message_input_controller_test.dart b/packages/stream_chat_flutter_core/test/message_input_controller_test.dart index 842a58bb..5e7934bd 100644 --- a/packages/stream_chat_flutter_core/test/message_input_controller_test.dart +++ b/packages/stream_chat_flutter_core/test/message_input_controller_test.dart @@ -3,28 +3,12 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; void main() { testWidgets( - 'should instantiate a new MessageInputController with default validator' - ' and empty message', + 'should instantiate a new MessageInputController with empty message', (tester) async { - final controller = MessageInputController(); + final controller = MessageInputController()..text = 'test'; - expect(controller.isValid, false); - controller.text = 'test'; - expect(controller.isValid, true); - }, - ); - - testWidgets( - 'should instantiate a new MessageInputController with default validator' - ' and specified message', - (tester) async { - final message = Message(text: 'test'); - final controller = MessageInputController( - message: message, - ); - - expect(controller.message, message); - expect(controller.isValid, true); + expect(controller.text, 'test'); + expect(controller.message.text, 'test'); }, ); } From a5005bb78f9fb5074d40c9ce6bb6b4643ef782e4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 16 Dec 2021 10:17:42 +0100 Subject: [PATCH 35/58] fix(ui): use file extension instead of mimeType --- .../lib/src/attachment_actions_modal.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index 0c10ae46..ac9213b6 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -365,12 +365,13 @@ class AttachmentActionsModal extends StatelessWidget { }) async { String? filePath; final appDocDir = await getTemporaryDirectory(); + final url = + attachment.assetUrl ?? attachment.imageUrl ?? attachment.thumbUrl!; await Dio().download( - attachment.assetUrl ?? attachment.imageUrl ?? attachment.thumbUrl!, + url, (Headers responseHeaders) { - final contentType = responseHeaders[Headers.contentTypeHeader]!; - final mimeType = contentType.first.split('/').last; - filePath ??= '${appDocDir.path}/${attachment.id}.$mimeType'; + final ext = Uri.parse(url).pathSegments.last; + filePath ??= '${appDocDir.path}/${attachment.id}.$ext'; return filePath!; }, onReceiveProgress: progressCallback, From 505b11b12e185041038674afa51aeef0416a73e4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 16 Dec 2021 10:19:29 +0100 Subject: [PATCH 36/58] chore(ui): update changelog --- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 383eeb02..4fc7439e 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- Use file extension instead of mimeType for downloading files + ## 3.3.2 - Updated `stream_chat_flutter_core` dependency to [`3.3.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog). From 2638b148d0b59d12cd67b7668149ef260da52e44 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 16 Dec 2021 17:11:07 +0530 Subject: [PATCH 37/58] added autoplay to videos --- .../lib/src/full_screen_media.dart | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 6547a93d..f56cc7dc 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -34,6 +34,7 @@ class FullScreenMedia extends StatefulWidget { String? userName, this.onShowMessage, this.attachmentActionsModalBuilder, + this.autoplayVideos = false, }) : userName = userName ?? '', super(key: key); @@ -57,6 +58,8 @@ class FullScreenMedia extends StatefulWidget { /// Use [defaultActionsModal.copyWith] to easily customize it final AttachmentActionsBuilder? attachmentActionsModalBuilder; + final bool autoplayVideos; + @override _FullScreenMediaState createState() => _FullScreenMediaState(); } @@ -81,7 +84,8 @@ class _FullScreenMediaState extends State ); _pageController = PageController(initialPage: widget.startIndex); _currentPage = widget.startIndex; - for (final attachment in widget.mediaAttachments) { + for (var i = 0; i < widget.mediaAttachments.length; i++) { + final attachment = widget.mediaAttachments[i]; if (attachment.type != 'video') continue; final package = VideoPackage(attachment, showControls: true); videoPackages[attachment.id] = package; @@ -90,9 +94,21 @@ class _FullScreenMediaState extends State } Future initializePlayers() async { + if (videoPackages.isEmpty) { + return; + } + + final currentAttachment = widget.mediaAttachments[widget.startIndex]; + await Future.wait(videoPackages.values.map( (it) => it.initialize(), )); + + if (widget.autoplayVideos && currentAttachment.type == 'video') { + final package = videoPackages.values + .firstWhere((e) => e._attachment == currentAttachment); + package._chewieController?.play(); + } setState(() {}); // ignore: no-empty-block } @@ -109,6 +125,24 @@ class _FullScreenMediaState extends State setState(() { _currentPage = val; }); + + if (videoPackages.isEmpty) { + return; + } + + final currentAttachment = widget.mediaAttachments[val]; + + for (final e in videoPackages.values) { + if (e._attachment != currentAttachment) { + e._chewieController?.pause(); + } + } + + if (widget.autoplayVideos && + currentAttachment.type == 'video') { + final controller = videoPackages[currentAttachment.id]!; + controller._chewieController?.play(); + } }, itemBuilder: (context, index) { final attachment = widget.mediaAttachments[index]; @@ -243,15 +277,16 @@ class _FullScreenMediaState extends State class VideoPackage { /// Constructor for creating [VideoPackage] VideoPackage( - Attachment attachment, { + this._attachment, { bool showControls = false, bool autoInitialize = true, }) : _showControls = showControls, _autoInitialize = autoInitialize, - _videoPlayerController = attachment.localUri != null - ? VideoPlayerController.file(File.fromUri(attachment.localUri!)) - : VideoPlayerController.network(attachment.assetUrl!); + _videoPlayerController = _attachment.localUri != null + ? VideoPlayerController.file(File.fromUri(_attachment.localUri!)) + : VideoPlayerController.network(_attachment.assetUrl!); + final Attachment _attachment; final bool _showControls; final bool _autoInitialize; final VideoPlayerController _videoPlayerController; From cb605673720c459e817f1e5e7e8f8b64e0d8a04f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 16 Dec 2021 17:13:55 +0530 Subject: [PATCH 38/58] added autoplay to videos --- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ packages/stream_chat_flutter/lib/src/full_screen_media.dart | 1 + 2 files changed, 7 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 383eeb02..0b4089c7 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +✅ Added + +- Videos can now be auto-played in `FullScreenMedia` + ## 3.3.2 - Updated `stream_chat_flutter_core` dependency to [`3.3.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog). diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index f56cc7dc..84ddb803 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -58,6 +58,7 @@ class FullScreenMedia extends StatefulWidget { /// Use [defaultActionsModal.copyWith] to easily customize it final AttachmentActionsBuilder? attachmentActionsModalBuilder; + /// Auto-play videos when page is opened final bool autoplayVideos; @override From 099e9046143bd3f5aa817d9d77b80ba783ad3a1e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 17 Dec 2021 21:50:34 +0530 Subject: [PATCH 39/58] fixed bugs --- .../lib/src/message_input/message_input.dart | 2 +- .../lib/src/message_input/stream_attachment_picker.dart | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 2ba5c038..e55fac19 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -538,7 +538,7 @@ class MessageInputState extends State ), ), ); - if (_isEditing) { + if (!_isEditing) { child = Material( elevation: 8, child: child, diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart index 7f6a6f6e..93c86d72 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart @@ -334,8 +334,10 @@ class _StreamAttachmentPickerState extends State { onMediaSelected: (media) { if (messageInputController.attachments .any((e) => e.id == media.id)) { - setState(() => messageInputController.attachments - .removeWhere((e) => e.id == media.id)); + messageInputController + .removeAttachmentById(media.id); + // setState(() => messageInputController.attachments + // .removeWhere((e) => e.id == media.id)); } else { _addAssetAttachment(media); } From 0aed43b1881703d3decc2909047e4f1d5db490b6 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 20 Dec 2021 10:55:51 +0100 Subject: [PATCH 40/58] fix(ui): use all message attachments in FullScreenMedia page --- .../lib/src/attachment/giphy_attachment.dart | 3 ++- .../lib/src/attachment/image_attachment.dart | 4 +++- .../lib/src/attachment/video_attachment.dart | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index fc0fb586..cc29f0b2 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -246,7 +246,8 @@ class GiphyAttachment extends AttachmentWidget { return StreamChannel( channel: channel, child: FullScreenMedia( - mediaAttachments: [attachment], + mediaAttachments: message.attachments, + startIndex: message.attachments.indexOf(attachment), userName: message.user?.name, message: message, onShowMessage: onShowMessage, diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index 41b8d039..ff01eea3 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -141,7 +141,9 @@ class ImageAttachment extends AttachmentWidget { return StreamChannel( channel: channel, child: FullScreenMedia( - mediaAttachments: [attachment], + mediaAttachments: message.attachments, + startIndex: + message.attachments.indexOf(attachment), userName: message.user?.name, message: message, onShowMessage: onShowMessage, diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index ce4ac804..0fc97d5e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -87,7 +87,9 @@ class VideoAttachment extends AttachmentWidget { builder: (_) => StreamChannel( channel: channel, child: FullScreenMedia( - mediaAttachments: [attachment], + mediaAttachments: message.attachments, + startIndex: + message.attachments.indexOf(attachment), userName: message.user?.name, message: message, onShowMessage: onShowMessage, From e19cc87362cceb06c1ac5dd72cf25c824085d7e3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 20 Dec 2021 16:11:12 +0530 Subject: [PATCH 41/58] feat(ui): add support for OG Attachment preview. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 14 +- .../stream_chat/lib/src/client/client.dart | 14 +- .../lib/src/core/api/message_api.dart | 14 +- .../lib/src/core/models/attachment.dart | 43 ++++ .../lib/src/message_input/message_input.dart | 185 +++++++++++++++--- 5 files changed, 231 insertions(+), 39 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 51034d3a..1925e325 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -493,6 +493,7 @@ class Channel { Future sendMessage( Message message, { bool skipPush = false, + bool skipEnrichUrl = false, }) async { _checkInitialized(); // Cancelling previous completer in case it's called again in the process @@ -540,6 +541,7 @@ class Channel { id!, type, skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, ); state!.addMessage(response.message); if (cooldown > 0) cooldownStartedAt = DateTime.now(); @@ -556,7 +558,10 @@ class Channel { /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually updating the message. - Future updateMessage(Message message) async { + Future updateMessage( + Message message, { + bool skipEnrichUrl = false, + }) async { final originalMessage = message; // Cancelling previous completer in case it's called again in the process @@ -594,7 +599,10 @@ class Channel { message = await attachmentsUploadCompleter.future; } - final response = await _client.updateMessage(message); + final response = await _client.updateMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); final m = response.message.copyWith( ownReactions: message.ownReactions, @@ -624,12 +632,14 @@ class Channel { Message message, { Map? set, List? unset, + bool skipEnrichUrl = false, }) async { try { final response = await _client.partialUpdateMessage( message.id, set: set, unset: unset, + skipEnrichUrl: skipEnrichUrl, ); final updatedMessage = response.message.copyWith( diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index b2ae3657..141bad48 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1169,12 +1169,14 @@ class StreamChatClient { String channelId, String channelType, { bool skipPush = false, + bool skipEnrichUrl = false, }) => _chatApi.message.sendMessage( channelId, channelType, message, skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, ); /// Lists all the message replies for the [parentId] @@ -1198,8 +1200,14 @@ class StreamChatClient { ); /// Update the given message - Future updateMessage(Message message) => - _chatApi.message.updateMessage(message); + Future updateMessage( + Message message, { + bool skipEnrichUrl = false, + }) => + _chatApi.message.updateMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); /// Partially update the given [messageId] /// Use [set] to define values to be set @@ -1208,11 +1216,13 @@ class StreamChatClient { String messageId, { Map? set, List? unset, + bool skipEnrichUrl = false, }) => _chatApi.message.partialUpdateMessage( messageId, set: set, unset: unset, + skipEnrichUrl: skipEnrichUrl, ); /// Deletes the given message diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 8894e14c..20cdfc51 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -16,12 +16,14 @@ class MessageApi { String channelType, Message message, { bool skipPush = false, + bool skipEnrichUrl = false, }) async { final response = await _client.post( '/channels/$channelType/$channelId/message', data: { 'message': message, 'skip_push': skipPush, + 'skip_enrich_url': skipEnrichUrl, }, ); return SendMessageResponse.fromJson(response.data); @@ -51,11 +53,15 @@ class MessageApi { /// Updates the given [message] Future updateMessage( - Message message, - ) async { + Message message, { + bool skipEnrichUrl = false, + }) async { final response = await _client.post( '/messages/${message.id}', - data: {'message': message}, + data: { + 'message': message, + 'skip_enrich_url': skipEnrichUrl, + }, ); return UpdateMessageResponse.fromJson(response.data); } @@ -67,12 +73,14 @@ class MessageApi { String messageId, { Map? set, List? unset, + bool skipEnrichUrl = false, }) async { final response = await _client.put( '/messages/$messageId', data: { if (set != null) 'set': set, if (unset != null) 'unset': unset, + 'skip_enrich_url': skipEnrichUrl, }, ); return UpdateMessageResponse.fromJson(response.data); diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index 09fe1083..325dbe11 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/models/action.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; @@ -66,6 +67,21 @@ class Attachment extends Equatable { topLevelFields + dbSpecificTopLevelFields, )); + factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) => + Attachment( + type: ogAttachment.type, + title: ogAttachment.title, + titleLink: ogAttachment.titleLink, + text: ogAttachment.text, + imageUrl: ogAttachment.imageUrl, + thumbUrl: ogAttachment.thumbUrl, + authorName: ogAttachment.authorName, + authorLink: ogAttachment.authorLink, + assetUrl: ogAttachment.assetUrl, + ogScrapeUrl: ogAttachment.ogScrapeUrl, + uploadState: const UploadState.success(), + ); + ///The attachment type based on the URL resource. This can be: audio, ///image or video final String? type; @@ -229,6 +245,33 @@ class Attachment extends Equatable { extraData: extraData ?? this.extraData, ); + Attachment merge(Attachment? other) { + if (other == null) return this; + return copyWith( + type: other.type, + titleLink: other.titleLink, + title: other.title, + thumbUrl: other.thumbUrl, + text: other.text, + pretext: other.pretext, + ogScrapeUrl: other.ogScrapeUrl, + imageUrl: other.imageUrl, + footerIcon: other.footerIcon, + footer: other.footer, + fields: other.fields, + fallback: other.fallback, + color: other.color, + authorName: other.authorName, + authorLink: other.authorLink, + authorIcon: other.authorIcon, + assetUrl: other.assetUrl, + actions: other.actions, + file: other.file, + uploadState: other.uploadState, + extraData: other.extraData, + ); + } + @override List get props => [ id, diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 2ba5c038..46072348 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -354,6 +354,7 @@ class MessageInputState extends State final _imagePicker = ImagePicker(); late final _focusNode = widget.focusNode ?? FocusNode(); bool _inputEnabled = true; + bool get _commandEnabled => _effectiveController.value.command != null; bool _showCommandsOverlay = false; bool _showMentionsOverlay = false; @@ -371,6 +372,7 @@ class MessageInputState extends State _effectiveController.value.status != MessageSendingStatus.sending; RestorableMessageInputController? _controller; + MessageInputController get _effectiveController => widget.messageInputController ?? _controller!.value; @@ -517,6 +519,14 @@ class MessageInputState extends State ), ], ), + ) + else if (_ogAttachment != null) + OGAttachmentPreview( + attachment: _ogAttachment!, + onDismissPreviewPressed: () { + setState(() => _ogAttachment = null); + _focusNode.unfocus(); + }, ), Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -538,7 +548,7 @@ class MessageInputState extends State ), ), ); - if (_isEditing) { + if (!_isEditing) { child = Material( elevation: 8, child: child, @@ -896,6 +906,7 @@ class MessageInputState extends State _actionsShrunk = value.isNotEmpty && actionsLength > 1; }); + _checkContainsUrlDebounced.call([value, context]); _checkCommands(value, context); _checkMentions(value, context); _checkEmoji(value, context); @@ -918,14 +929,52 @@ class MessageInputState extends State return context.translations.writeAMessageLabel; } - void _checkEmoji(String s, BuildContext context) { - if (s.isNotEmpty && + Attachment? _ogAttachment; + String? _lastSearchedContainsUrlText; + CancelableOperation? _enrichUrlOperation; + + late final _checkContainsUrlDebounced = debounce( + (String value, BuildContext context) async { + // Cancel the previous operation if it's still running + _enrichUrlOperation?.cancel(); + + // If the text is same as the last time, don't do anything + if (_lastSearchedContainsUrlText == value) return; + _lastSearchedContainsUrlText = value; + + final regex = + RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'); + final matchedUrls = regex.allMatches(value); + if (matchedUrls.isEmpty) return; + + final firstMatchedUrl = matchedUrls.first.group(0)!; + + // If the parsed url matches the ogAttachment url, don't do anything + if (_ogAttachment?.titleLink == firstMatchedUrl) return; + + final client = StreamChat.of(context).client; + + _enrichUrlOperation = CancelableOperation.fromFuture( + client.enrichUrl(firstMatchedUrl).then((ogAttachment) { + final attachment = Attachment.fromOGAttachment(ogAttachment); + setState(() => _ogAttachment = attachment); + }).onError((error, stackTrace) { + // Reset the ogAttachment if there was an error + setState(() => _ogAttachment = null); + if (error != null) { + widget.onError?.call(error, stackTrace); + } + }), + ); + }, + const Duration(seconds: 1), + ); + + void _checkEmoji(String value, BuildContext context) { + if (value.isNotEmpty && _effectiveController.baseOffset > 0 && _effectiveController.text - .substring( - 0, - _effectiveController.baseOffset, - ) + .substring(0, _effectiveController.baseOffset) .contains(':')) { final textToSelection = _effectiveController.text.substring( 0, @@ -941,14 +990,11 @@ class MessageInputState extends State } } - void _checkMentions(String s, BuildContext context) { - if (s.isNotEmpty && + void _checkMentions(String value, BuildContext context) { + if (value.isNotEmpty && _effectiveController.baseOffset > 0 && _effectiveController.text - .substring( - 0, - _effectiveController.baseOffset, - ) + .substring(0, _effectiveController.baseOffset) .split(' ') .last .contains('@')) { @@ -964,11 +1010,11 @@ class MessageInputState extends State } } - void _checkCommands(String s, BuildContext context) { - if (s.startsWith('/')) { + void _checkCommands(String value, BuildContext context) { + if (value.startsWith('/')) { final allCommands = StreamChannel.of(context).channel.config?.commands; final command = - allCommands?.firstWhereOrNull((it) => it.name == s.substring(1)); + allCommands?.firstWhereOrNull((it) => it.name == value.substring(1)); if (command != null) { return _setCommand(command); } else if (!_showCommandsOverlay) { @@ -1030,10 +1076,7 @@ class MessageInputState extends State } final splits = _effectiveController.text - .substring( - 0, - _effectiveController.selectionStart, - ) + .substring(0, _effectiveController.selectionStart) .split('@'); final query = splits.last.toLowerCase(); @@ -1082,10 +1125,7 @@ class MessageInputState extends State } final splits = _effectiveController.text - .substring( - 0, - _effectiveController.baseOffset, - ) + .substring(0, _effectiveController.baseOffset) .split(':'); final query = splits.last.toLowerCase(); @@ -1222,11 +1262,7 @@ class MessageInputState extends State focusElevation: 0, hoverElevation: 0, onPressed: () { - _effectiveController.value = _effectiveController.value.copyWith( - attachments: _effectiveController.attachments - .where((it) => it.id != attachment.id) - .toList(), - ); + _effectiveController.removeAttachmentById(attachment.id); }, fillColor: _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), @@ -1572,10 +1608,19 @@ class MessageInputState extends State Future sendMessage() async { var message = _effectiveController.value; + // Add ogAttachment if present + final skipEnrichUrl = _ogAttachment == null; + if (!skipEnrichUrl) { + message = message.copyWith( + attachments: [...message.attachments, _ogAttachment!], + ); + } + var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; shouldKeepFocus ??= !_commandEnabled; + _ogAttachment = null; _effectiveController.reset(); if (widget.preMessageSending != null) { @@ -1590,10 +1635,16 @@ class MessageInputState extends State try { Future sendingFuture; - if (!_isEditing) { - sendingFuture = channel.sendMessage(message); + if (_isEditing) { + sendingFuture = channel.updateMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); } else { - sendingFuture = channel.updateMessage(message); + sendingFuture = channel.sendMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); } if (shouldKeepFocus) { @@ -1702,3 +1753,73 @@ class MessageInputState extends State super.didChangeDependencies(); } } + +class OGAttachmentPreview extends StatelessWidget { + const OGAttachmentPreview({ + Key? key, + required this.attachment, + this.onDismissPreviewPressed, + }) : super(key: key); + + final Attachment attachment; + final VoidCallback? onDismissPreviewPressed; + + @override + Widget build(BuildContext context) { + final chatTheme = StreamChatTheme.of(context); + final textTheme = chatTheme.textTheme; + final colorTheme = chatTheme.colorTheme; + + final attachmentTitle = attachment.title; + final attachmentText = attachment.text; + + return Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.link, + color: colorTheme.accentPrimary, + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: colorTheme.accentPrimary, + width: 2, + ), + ), + ), + padding: const EdgeInsets.only(left: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (attachmentTitle != null) + Text( + attachmentTitle.trim(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.body.copyWith(fontWeight: FontWeight.w700), + ), + if (attachmentText != null) + Text( + attachmentText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.body.copyWith(fontWeight: FontWeight.w400), + ), + ], + ), + ), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: onDismissPreviewPressed, + ), + ], + ); + } +} From 50e9cb38e9033a95ee5f27baffc4dc1ec5f645cc Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 20 Dec 2021 16:40:58 +0530 Subject: [PATCH 42/58] chore(ui): minor fixes Signed-off-by: xsahil03x --- .../lib/src/message_input/message_input.dart | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 46072348..a28754d3 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -942,10 +942,15 @@ class MessageInputState extends State if (_lastSearchedContainsUrlText == value) return; _lastSearchedContainsUrlText = value; - final regex = - RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'); - final matchedUrls = regex.allMatches(value); - if (matchedUrls.isEmpty) return; + final matchedUrls = + RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+') + .allMatches(value); + + // Reset the og attachment if the text doesn't contain any url + if (matchedUrls.isEmpty) { + setState(() => _ogAttachment = null); + return; + } final firstMatchedUrl = matchedUrls.first.group(0)!; @@ -955,19 +960,20 @@ class MessageInputState extends State final client = StreamChat.of(context).client; _enrichUrlOperation = CancelableOperation.fromFuture( - client.enrichUrl(firstMatchedUrl).then((ogAttachment) { + client.enrichUrl(firstMatchedUrl), + ).then( + (ogAttachment) { final attachment = Attachment.fromOGAttachment(ogAttachment); setState(() => _ogAttachment = attachment); - }).onError((error, stackTrace) { + }, + onError: (error, stackTrace) { // Reset the ogAttachment if there was an error setState(() => _ogAttachment = null); - if (error != null) { - widget.onError?.call(error, stackTrace); - } - }), + widget.onError?.call(error, stackTrace); + }, ); }, - const Duration(seconds: 1), + const Duration(milliseconds: 650), ); void _checkEmoji(String value, BuildContext context) { From 56e030f40c83ab8cb86f9e0ac88ce657e72ececf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 20 Dec 2021 18:25:56 +0530 Subject: [PATCH 43/58] feat(ui, core): handle ogAttachment modification via message_input_controller.dart. Signed-off-by: xsahil03x --- .../lib/src/message_input/message_input.dart | 47 +++--------- .../lib/src/message_input_controller.dart | 64 +++++++++++++---- .../src/message_text_field_controller.dart | 72 +++++++++++++++++++ 3 files changed, 133 insertions(+), 50 deletions(-) create mode 100644 packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index a28754d3..7e02e4b3 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -335,17 +335,6 @@ class MessageInput extends StatefulWidget { @override MessageInputState createState() => MessageInputState(); - - /// Use this method to get the current [StreamChatState] instance - static MessageInputState of(BuildContext context) { - MessageInputState? messageInputState; - messageInputState = context.findAncestorStateOfType(); - assert( - messageInputState != null, - 'You must have a MessageInput widget as ancestor of your widget tree', - ); - return messageInputState!; - } } /// State of [MessageInput] @@ -520,11 +509,11 @@ class MessageInputState extends State ], ), ) - else if (_ogAttachment != null) + else if (_effectiveController.ogAttachment != null) OGAttachmentPreview( - attachment: _ogAttachment!, + attachment: _effectiveController.ogAttachment!, onDismissPreviewPressed: () { - setState(() => _ogAttachment = null); + _effectiveController.clearOGAttachment(); _focusNode.unfocus(); }, ), @@ -929,7 +918,6 @@ class MessageInputState extends State return context.translations.writeAMessageLabel; } - Attachment? _ogAttachment; String? _lastSearchedContainsUrlText; CancelableOperation? _enrichUrlOperation; @@ -948,14 +936,16 @@ class MessageInputState extends State // Reset the og attachment if the text doesn't contain any url if (matchedUrls.isEmpty) { - setState(() => _ogAttachment = null); + _effectiveController.clearOGAttachment(); return; } final firstMatchedUrl = matchedUrls.first.group(0)!; // If the parsed url matches the ogAttachment url, don't do anything - if (_ogAttachment?.titleLink == firstMatchedUrl) return; + if (_effectiveController.ogAttachment?.titleLink == firstMatchedUrl) { + return; + } final client = StreamChat.of(context).client; @@ -964,11 +954,11 @@ class MessageInputState extends State ).then( (ogAttachment) { final attachment = Attachment.fromOGAttachment(ogAttachment); - setState(() => _ogAttachment = attachment); + _effectiveController.setOGAttachment(attachment); }, onError: (error, stackTrace) { // Reset the ogAttachment if there was an error - setState(() => _ogAttachment = null); + _effectiveController.clearOGAttachment(); widget.onError?.call(error, stackTrace); }, ); @@ -1470,14 +1460,6 @@ class MessageInputState extends State } } - /// Add an attachment to the sending message - /// Use this to add custom type attachments - /// - /// Note: Only meant to be used from outside the state. - void addAttachment(Attachment attachment) { - _addAttachments([attachment]); - } - /// Adds an attachment to the [messageInputController.attachments] map void _addAttachments(Iterable attachments) { final limit = widget.attachmentLimit; @@ -1612,21 +1594,14 @@ class MessageInputState extends State /// Sends the current message Future sendMessage() async { - var message = _effectiveController.value; + final skipEnrichUrl = _effectiveController.ogAttachment == null; - // Add ogAttachment if present - final skipEnrichUrl = _ogAttachment == null; - if (!skipEnrichUrl) { - message = message.copyWith( - attachments: [...message.attachments, _ogAttachment!], - ); - } + var message = _effectiveController.value; var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; shouldKeepFocus ??= !_commandEnabled; - _ogAttachment = null; _effectiveController.reset(); if (widget.preMessageSending != null) { diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 29bd2db2..f218b9ca 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -1,8 +1,9 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter_core/src/message_text_field_controller.dart'; /// A value listenable builder related to a [Message]. /// @@ -17,33 +18,46 @@ class MessageInputController extends ValueNotifier { /// message. factory MessageInputController({ Message? message, + Map? textPatternStyle, }) => MessageInputController._( initialMessage: message ?? Message(), + textPatternStyle: textPatternStyle, ); /// Creates a controller for an editable text field from an initial [text]. - factory MessageInputController.fromText(String? text) => + factory MessageInputController.fromText( + String? text, { + Map? textPatternStyle, + }) => MessageInputController._( initialMessage: Message(text: text), + textPatternStyle: textPatternStyle, ); /// Creates a controller for an editable text field from initial /// [attachments]. factory MessageInputController.fromAttachments( - List attachments, - ) => + List attachments, { + Map? textPatternStyle, + }) => MessageInputController._( initialMessage: Message(attachments: attachments), + textPatternStyle: textPatternStyle, ); MessageInputController._({ required Message initialMessage, - }) : _textEditingController = - TextEditingController.fromValue(TextEditingValue( - text: initialMessage.text ?? '', - composing: TextRange.collapsed(initialMessage.text?.length ?? 0), - )), + Map? textPatternStyle, + }) : _textEditingController = MessageTextFieldController.fromValue( + initialMessage.text == null + ? const TextEditingValue() + : TextEditingValue( + text: initialMessage.text!, + composing: TextRange.collapsed(initialMessage.text!.length), + ), + textPatternStyle: textPatternStyle, + ), _initialMessage = initialMessage, super(initialMessage) { addListener(_textEditingSyncer); @@ -73,8 +87,9 @@ class MessageInputController extends ValueNotifier { Message get message => value; /// Returns the controller of the text field linked to this controller. - TextEditingController get textEditingController => _textEditingController; - final TextEditingController _textEditingController; + MessageTextFieldController get textEditingController => + _textEditingController; + final MessageTextFieldController _textEditingController; /// Returns the text of the message. String get text => _textEditingController.text; @@ -174,6 +189,28 @@ class MessageInputController extends ValueNotifier { attachments = []; } + // Only used to store the value locally in order to remove it if we call + // [clearOGAttachment] or [setOGAttachment] again. + Attachment? _ogAttachment; + + /// Returns the og attachment of the message if set + Attachment? get ogAttachment => + attachments.firstWhereOrNull((it) => it.id == _ogAttachment?.id); + + /// Sets the og attachment in the message. + void setOGAttachment(Attachment attachment) { + attachments = [...attachments] + ..remove(_ogAttachment) + ..insert(0, attachment); + _ogAttachment = attachment; + } + + /// Removes the og attachment. + void clearOGAttachment() { + attachments = [...attachments]..remove(_ogAttachment); + _ogAttachment = null; + } + /// Returns the list of mentioned users in the message. List get mentionedUsers => value.mentionedUsers; @@ -220,9 +257,8 @@ class MessageInputController extends ValueNotifier { /// Sets the [value] to the initial [Message] value. void reset({bool resetId = true}) { if (resetId) { - _initialMessage = _initialMessage.copyWith( - id: const Uuid().v4(), - ); + final newId = const Uuid().v4(); + _initialMessage = _initialMessage.copyWith(id: newId); } value = _initialMessage; } diff --git a/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart new file mode 100644 index 00000000..82a60709 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +class MessageTextFieldController extends TextEditingController { + MessageTextFieldController({ + String? text, + this.textPatternStyle, + }) : super(text: text); + + MessageTextFieldController.fromValue( + TextEditingValue? value, { + this.textPatternStyle, + }) : super.fromValue(value); + + final Map? textPatternStyle; + + /// + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + required bool withComposing, + }) { + final pattern = textPatternStyle; + if (pattern == null) { + return super.buildTextSpan( + context: context, + style: style, + withComposing: withComposing, + ); + } + + return TextSpan(text: text, style: style).splitMapJoin( + RegExp(pattern.keys.map((it) => it.pattern).join('|')), + onMatch: (match) { + final text = match[0]!; + final key = pattern.keys.firstWhere((it) => it.hasMatch(text)); + return TextSpan( + text: text, + style: pattern[key], + ); + }, + ); + } +} + +extension _TextSpanX on TextSpan { + TextSpan splitMapJoin( + Pattern pattern, { + TextSpan Function(Match)? onMatch, + TextSpan Function(TextSpan)? onNonMatch, + }) { + final children = []; + + toPlainText().splitMapJoin( + pattern, + onMatch: (match) { + final span = TextSpan(text: match.group(0), style: style); + final updated = onMatch?.call(match); + children.add(updated ?? span); + return span.toPlainText(); + }, + onNonMatch: (text) { + final span = TextSpan(text: text, style: style); + final updatedSpan = onNonMatch?.call(span); + children.add(updatedSpan ?? span); + return span.toPlainText(); + }, + ); + + return TextSpan(style: style, children: children); + } +} From 0c2ce9c955e8e1621fb0132780dc1d297508967b Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 20 Dec 2021 19:29:03 +0530 Subject: [PATCH 44/58] fixed docs --- .../lib/src/core/models/message.dart | 6 +- .../test/src/client/channel_test.dart | 1 - .../stream_attachment_picker.dart | 93 +++++++++++-------- .../stream_message_text_field.dart | 7 +- .../lib/stream_chat_flutter.dart | 8 +- 5 files changed, 66 insertions(+), 49 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index a7d21c03..1004b7a0 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -305,7 +305,8 @@ class Message extends Equatable { quotedMessage != null && quotedMessage is! _NullConst) { throw ArgumentError( - '`quotedMessage` can only be set as Message or null'); + '`quotedMessage` can only be set as Message or null', + ); } return true; }(), 'Validate type for quotedMessage'); @@ -315,7 +316,8 @@ class Message extends Equatable { quotedMessageId != null && quotedMessageId is! _NullConst) { throw ArgumentError( - '`quotedMessage` can only be set as String or null'); + '`quotedMessage` can only be set as String or null', + ); } return true; }(), 'Validate type for quotedMessage'); diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 60e8dd93..7eb68b79 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -756,7 +756,6 @@ void main() { const messageId = 'test-message-id'; final message = Message( id: messageId, - status: MessageSendingStatus.sending, ); expectLater( diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart index 93c86d72..6f03c976 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart @@ -23,41 +23,8 @@ typedef CustomAttachmentIconBuilder = Widget Function( /// A widget that allows to pick an attachment. class StreamAttachmentPicker extends StatefulWidget { - /// True if the picker is open. - final bool isOpen; - - /// The picker size in height. - final double pickerSize; - - /// The [MessageInputController] linked to this picker. - final MessageInputController messageInputController; - - /// The limit of attachments that can be picked. - final int attachmentLimit; - - /// The callback for when the attachment limit is exceeded. - final AttachmentLimitExceedListener? onAttachmentLimitExceeded; - - final ValueChanged? onError; - final FilePickerCallback onFilePicked; - - /// Video quality to use when compressing the videos. - final VideoQuality compressedVideoQuality; - - /// Frame rate to use when compressing the videos. - final int compressedVideoFrameRate; - - /// Max attachment size in bytes: - /// - Defaults to 20 MB - /// - Do not set it if you're using our default CDN - final int maxAttachmentSize; - - /// The list of attachment types that can be picked. - final List allowedAttachmentTypes; - - /// The list of custom attachment types that can be picked. - final List customAttachmentTypes; - + /// Default constructor for [StreamAttachmentPicker] which creates the Stream + /// attachment picker widget. const StreamAttachmentPicker({ Key? key, required this.messageInputController, @@ -78,6 +45,46 @@ class StreamAttachmentPicker extends StatefulWidget { this.customAttachmentTypes = const [], }) : super(key: key); + /// True if the picker is open. + final bool isOpen; + + /// The picker size in height. + final double pickerSize; + + /// The [MessageInputController] linked to this picker. + final MessageInputController messageInputController; + + /// The limit of attachments that can be picked. + final int attachmentLimit; + + /// The callback for when the attachment limit is exceeded. + final AttachmentLimitExceedListener? onAttachmentLimitExceeded; + + /// Callback for when an error occurs in the attachment picker. + final ValueChanged? onError; + + /// Callback for when file is picked. + final FilePickerCallback onFilePicked; + + /// Video quality to use when compressing the videos. + final VideoQuality compressedVideoQuality; + + /// Frame rate to use when compressing the videos. + final int compressedVideoFrameRate; + + /// Max attachment size in bytes: + /// - Defaults to 20 MB + /// - Do not set it if you're using our default CDN + final int maxAttachmentSize; + + /// The list of attachment types that can be picked. + final List allowedAttachmentTypes; + + /// The list of custom attachment types that can be picked. + final List customAttachmentTypes; + + /// Used to create a new copy of [StreamAttachmentPicker] with modified + /// properties. StreamAttachmentPicker copyWith({ Key? key, MessageInputController? messageInputController, @@ -549,14 +556,22 @@ class _PickerWidgetState extends State<_PickerWidget> { } } +/// Class which holds data for a custom attachment type in the attachment picker class CustomAttachmentType { - String type; - CustomAttachmentIconBuilder iconBuilder; - WidgetBuilder pickerBuilder; - + /// Default constructor for creating a custom attachment for the attachment + /// picker. CustomAttachmentType({ required this.type, required this.iconBuilder, required this.pickerBuilder, }); + + /// Type name. + String type; + + /// Builds the icon in the attachment picker top row. + CustomAttachmentIconBuilder iconBuilder; + + /// Builds content in the attachment builder when icon is selected. + WidgetBuilder pickerBuilder; } diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index e7d765d9..af06f53b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -1,4 +1,4 @@ -// ignore_for_file: prefer-trailing-comma, cascade_invocations +// ignore_for_file: prefer-trailing-comma, cascade_invocations, lines_longer_than_80_chars import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; @@ -44,7 +44,8 @@ class StreamMessageTextField extends StatefulWidget { /// 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], + /// 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. /// @@ -633,7 +634,7 @@ class StreamMessageTextField extends StatefulWidget { defaultValue: null)); properties.add(DiagnosticsProperty( 'scrollPadding', scrollPadding, - defaultValue: const EdgeInsets.all(20.0))); + defaultValue: const EdgeInsets.all(20))); properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index b39a2eea..a4e9d715 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -23,16 +23,16 @@ export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; export 'src/mention_tile.dart'; export 'src/message_action.dart'; +export 'src/message_input/countdown_button.dart'; export 'src/message_input/message_input.dart'; +export 'src/message_input/stream_attachment_picker.dart'; +export 'src/message_input/stream_message_send_button.dart'; +export 'src/message_input/stream_message_text_field.dart'; export 'src/message_list_view.dart'; export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; -export 'src/message_input/countdown_button.dart'; -export 'src/message_input/stream_attachment_picker.dart'; -export 'src/message_input/stream_message_send_button.dart'; -export 'src/message_input/stream_message_text_field.dart'; export 'src/option_list_tile.dart'; export 'src/reaction_icon.dart'; export 'src/reaction_picker.dart'; From 8b6c732844c31ffa09e01ea91eeb4e02422fd607 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 21 Dec 2021 14:11:23 +0530 Subject: [PATCH 45/58] feat(ui, core): minor fixes and improvements Signed-off-by: xsahil03x --- .../lib/src/message_input/message_input.dart | 112 ++++++++++-------- .../lib/src/message_input_controller.dart | 9 +- 2 files changed, 68 insertions(+), 53 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 7e02e4b3..be63b10b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -895,7 +895,7 @@ class MessageInputState extends State _actionsShrunk = value.isNotEmpty && actionsLength > 1; }); - _checkContainsUrlDebounced.call([value, context]); + _checkContainsUrl(value, context); _checkCommands(value, context); _checkMentions(value, context); _checkEmoji(value, context); @@ -920,52 +920,65 @@ class MessageInputState extends State String? _lastSearchedContainsUrlText; CancelableOperation? _enrichUrlOperation; - - late final _checkContainsUrlDebounced = debounce( - (String value, BuildContext context) async { - // Cancel the previous operation if it's still running - _enrichUrlOperation?.cancel(); - - // If the text is same as the last time, don't do anything - if (_lastSearchedContainsUrlText == value) return; - _lastSearchedContainsUrlText = value; - - final matchedUrls = - RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+') - .allMatches(value); - - // Reset the og attachment if the text doesn't contain any url - if (matchedUrls.isEmpty) { - _effectiveController.clearOGAttachment(); - return; - } - - final firstMatchedUrl = matchedUrls.first.group(0)!; - - // If the parsed url matches the ogAttachment url, don't do anything - if (_effectiveController.ogAttachment?.titleLink == firstMatchedUrl) { - return; - } - - final client = StreamChat.of(context).client; - - _enrichUrlOperation = CancelableOperation.fromFuture( - client.enrichUrl(firstMatchedUrl), - ).then( - (ogAttachment) { - final attachment = Attachment.fromOGAttachment(ogAttachment); - _effectiveController.setOGAttachment(attachment); - }, - onError: (error, stackTrace) { - // Reset the ogAttachment if there was an error - _effectiveController.clearOGAttachment(); - widget.onError?.call(error, stackTrace); - }, - ); - }, - const Duration(milliseconds: 650), + final _urlRegex = RegExp( + r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+', ); + void _checkContainsUrl(String value, BuildContext context) async { + // Cancel the previous operation if it's still running + _enrichUrlOperation?.cancel(); + + // If the text is same as the last time, don't do anything + if (_lastSearchedContainsUrlText == value) return; + _lastSearchedContainsUrlText = value; + + final matchedUrls = _urlRegex.allMatches(value); + + // Reset the og attachment if the text doesn't contain any url + if (matchedUrls.isEmpty) { + _effectiveController.clearOGAttachment(); + return; + } + + final firstMatchedUrl = matchedUrls.first.group(0)!; + + // If the parsed url matches the ogAttachment url, don't do anything + if (_effectiveController.ogAttachment?.titleLink == firstMatchedUrl) { + return; + } + + final client = StreamChat.of(context).client; + + _enrichUrlOperation = CancelableOperation.fromFuture( + _enrichUrl(firstMatchedUrl, client), + ).then( + (ogAttachment) { + final attachment = Attachment.fromOGAttachment(ogAttachment); + _effectiveController.setOGAttachment(attachment); + }, + onError: (error, stackTrace) { + // Reset the ogAttachment if there was an error + _effectiveController.clearOGAttachment(); + widget.onError?.call(error, stackTrace); + }, + ); + } + + final _ogAttachmentCache = {}; + + Future _enrichUrl( + String url, + StreamChatClient client, + ) async { + var response = _ogAttachmentCache[url]; + if (response == null) { + final client = StreamChat.of(context).client; + response = await client.enrichUrl(url); + _ogAttachmentCache[url] = response; + } + return response; + } + void _checkEmoji(String value, BuildContext context) { if (value.isNotEmpty && _effectiveController.baseOffset > 0 && @@ -1169,11 +1182,14 @@ class MessageInputState extends State } Widget _buildAttachments() { - if (_effectiveController.attachments.isEmpty) return const Offstage(); - final fileAttachments = _effectiveController.attachments + final nonOGAttachments = _effectiveController.attachments.where( + (it) => it.titleLink == null, + ); + if (nonOGAttachments.isEmpty) return const Offstage(); + final fileAttachments = nonOGAttachments .where((it) => it.type == 'file') .toList(growable: false); - final remainingAttachments = _effectiveController.attachments + final remainingAttachments = nonOGAttachments .where((it) => it.type != 'file') .toList(growable: false); return Column( diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index f218b9ca..6af8db6e 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -66,10 +66,7 @@ class MessageInputController extends ValueNotifier { void _textEditingSyncer() { final cleanText = value.command == null ? value.text - : value.text?.replaceFirst( - '/${value.command} ', - '', - ); + : value.text?.replaceFirst('/${value.command} ', ''); if (cleanText != _textEditingController.text) { final previousOffset = _textEditingController.value.selection.start; @@ -207,7 +204,9 @@ class MessageInputController extends ValueNotifier { /// Removes the og attachment. void clearOGAttachment() { - attachments = [...attachments]..remove(_ogAttachment); + if (_ogAttachment != null) { + removeAttachment(_ogAttachment!); + } _ogAttachment = null; } From 3f0eb33d6e0067099fdf4f15a84e8617e793f09a Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 21 Dec 2021 15:56:53 +0530 Subject: [PATCH 46/58] cleanup --- .../lib/src/message_input/stream_attachment_picker.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart index 6f03c976..909f9561 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart @@ -343,8 +343,6 @@ class _StreamAttachmentPickerState extends State { .any((e) => e.id == media.id)) { messageInputController .removeAttachmentById(media.id); - // setState(() => messageInputController.attachments - // .removeWhere((e) => e.id == media.id)); } else { _addAssetAttachment(media); } From 3c6f345c2d6c53eb470774536b6f87b54084e19e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 21 Dec 2021 16:20:32 +0530 Subject: [PATCH 47/58] fixed tests for modal --- .../test/src/message_action_modal_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index 9210ac49..cfd6f4ec 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/src/message_actions_modal.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'mocks.dart'; @@ -37,6 +38,7 @@ void main() { user: User( id: 'user-id', ), + status: MessageSendingStatus.sent, ), messageWidget: const Text( 'test', @@ -196,6 +198,7 @@ void main() { user: User( id: 'user-id', ), + status: MessageSendingStatus.sent, ), messageTheme: streamTheme.ownMessageTheme, ), @@ -242,6 +245,7 @@ void main() { user: User( id: 'user-id', ), + status: MessageSendingStatus.sent, ), messageTheme: streamTheme.ownMessageTheme, ), From 9652c977a27e770981128d06dc2e758770a12b30 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 27 Dec 2021 18:00:48 +0100 Subject: [PATCH 48/58] fix tests --- packages/stream_chat/lib/src/core/models/message.dart | 4 ++-- .../lib/src/message_input/stream_message_text_field.dart | 1 - .../test/src/attachment_actions_modal_test.dart | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 1004b7a0..017bb06f 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -414,8 +414,8 @@ class Message extends Equatable { shadowed, silent, command, - createdAt, - updatedAt, + _createdAt, + _updatedAt, deletedAt, user, pinned, diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index af06f53b..61466aaf 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -703,7 +703,6 @@ class _StreamMessageTextFieldState extends State @override Widget build(BuildContext context) => TextField( - key: widget.key, controller: _effectiveController.textEditingController, onChanged: (newText) { _effectiveController.text = newText; diff --git a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart index c8832f6f..3b711a85 100644 --- a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/attachment_actions_modal.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; From 8278e1f7452dddfebfb12d791a7e6bbc31848fe7 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 29 Dec 2021 11:42:08 +0100 Subject: [PATCH 49/58] fix analysis --- .../lib/src/message_input/message_input.dart | 7 ++++++- .../lib/src/message_text_field_controller.dart | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index be63b10b..d0d5e882 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -1751,14 +1751,19 @@ class MessageInputState extends State } } +/// Preview of an Open Graph attachment. class OGAttachmentPreview extends StatelessWidget { + /// Returns a new instance of [OGAttachmentPreview] const OGAttachmentPreview({ Key? key, required this.attachment, this.onDismissPreviewPressed, }) : super(key: key); + /// The attachment to be rendered. final Attachment attachment; + + /// Called when the dismiss button is pressed. final VoidCallback? onDismissPreviewPressed; @override @@ -1773,7 +1778,7 @@ class OGAttachmentPreview extends StatelessWidget { return Row( children: [ Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Icon( Icons.link, color: colorTheme.accentPrimary, diff --git a/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart index 82a60709..30065ff4 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart @@ -1,19 +1,24 @@ import 'package:flutter/material.dart'; +/// Controller for the [StreamTextField] widget. class MessageTextFieldController extends TextEditingController { + /// Returns a new MessageTextFieldController MessageTextFieldController({ String? text, this.textPatternStyle, }) : super(text: text); + /// Returns a new MessageTextFieldController with the given text [value]. MessageTextFieldController.fromValue( TextEditingValue? value, { this.textPatternStyle, }) : super.fromValue(value); + /// A map of style to apply to the text matching the RegExp patterns. final Map? textPatternStyle; - /// + /// Builds a [TextSpan] from the current text, + /// highlighting the matches for [textPatternStyle]. @override TextSpan buildTextSpan({ required BuildContext context, From e85b1a8b3f7f24417e08b12c348fb1207024e602 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 29 Dec 2021 15:19:27 +0100 Subject: [PATCH 50/58] move message input controllers to ui --- .../lib/src/message_input}/message_input_controller.dart | 3 +-- .../lib/src/message_input}/message_text_field_controller.dart | 0 .../lib/src/message_input/stream_message_text_field.dart | 2 +- packages/stream_chat_flutter/lib/stream_chat_flutter.dart | 2 ++ .../test/src/message_input}/message_input_controller_test.dart | 2 +- .../stream_chat_flutter_core/lib/stream_chat_flutter_core.dart | 1 - 6 files changed, 5 insertions(+), 5 deletions(-) rename packages/{stream_chat_flutter_core/lib/src => stream_chat_flutter/lib/src/message_input}/message_input_controller.dart (98%) rename packages/{stream_chat_flutter_core/lib/src => stream_chat_flutter/lib/src/message_input}/message_text_field_controller.dart (100%) rename packages/{stream_chat_flutter_core/test => stream_chat_flutter/test/src/message_input}/message_input_controller_test.dart (82%) diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart similarity index 98% rename from packages/stream_chat_flutter_core/lib/src/message_input_controller.dart rename to packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart index 6af8db6e..45599239 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart @@ -2,8 +2,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter_core/src/message_text_field_controller.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A value listenable builder related to a [Message]. /// diff --git a/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart similarity index 100% rename from packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart rename to packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index 61466aaf..8e4a34eb 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -8,7 +8,7 @@ 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'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; export 'package:flutter/services.dart' show diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index a4e9d715..ad3f968d 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -25,6 +25,8 @@ export 'src/mention_tile.dart'; export 'src/message_action.dart'; export 'src/message_input/countdown_button.dart'; export 'src/message_input/message_input.dart'; +export 'src/message_input/message_input_controller.dart'; +export 'src/message_input/message_text_field_controller.dart'; export 'src/message_input/stream_attachment_picker.dart'; export 'src/message_input/stream_message_send_button.dart'; export 'src/message_input/stream_message_text_field.dart'; diff --git a/packages/stream_chat_flutter_core/test/message_input_controller_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart similarity index 82% rename from packages/stream_chat_flutter_core/test/message_input_controller_test.dart rename to packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart index 5e7934bd..1b496784 100644 --- a/packages/stream_chat_flutter_core/test/message_input_controller_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { testWidgets( diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart index bf191ace..8bd41c76 100644 --- a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -7,7 +7,6 @@ export 'src/better_stream_builder.dart'; export 'src/channel_list_core.dart' hide ChannelListCoreState; export 'src/channels_bloc.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_search_bloc.dart'; export 'src/message_search_list_core.dart' hide MessageSearchListCoreState; From fbf8694f582d277da6ead368e3408d20dbc18d51 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 3 Jan 2022 09:20:47 +0100 Subject: [PATCH 51/58] use a function for textPatternStyle and add default to theme --- .../message_input_controller.dart | 8 ++++---- .../message_text_field_controller.dart | 18 ++++++++++++++---- .../lib/src/stream_chat_theme.dart | 1 + .../lib/src/theme/message_input_theme.dart | 18 +++++++++++++++--- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart index 45599239..185c875c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart @@ -17,7 +17,7 @@ class MessageInputController extends ValueNotifier { /// message. factory MessageInputController({ Message? message, - Map? textPatternStyle, + Map? textPatternStyle, }) => MessageInputController._( initialMessage: message ?? Message(), @@ -27,7 +27,7 @@ class MessageInputController extends ValueNotifier { /// Creates a controller for an editable text field from an initial [text]. factory MessageInputController.fromText( String? text, { - Map? textPatternStyle, + Map? textPatternStyle, }) => MessageInputController._( initialMessage: Message(text: text), @@ -38,7 +38,7 @@ class MessageInputController extends ValueNotifier { /// [attachments]. factory MessageInputController.fromAttachments( List attachments, { - Map? textPatternStyle, + Map? textPatternStyle, }) => MessageInputController._( initialMessage: Message(attachments: attachments), @@ -47,7 +47,7 @@ class MessageInputController extends ValueNotifier { MessageInputController._({ required Message initialMessage, - Map? textPatternStyle, + Map? textPatternStyle, }) : _textEditingController = MessageTextFieldController.fromValue( initialMessage.text == null ? const TextEditingValue() diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart index 30065ff4..12ec4831 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A function that takes a [BuildContext] and returns a [TextStyle]. +typedef TextStyleBuilder = TextStyle Function(BuildContext context); /// Controller for the [StreamTextField] widget. class MessageTextFieldController extends TextEditingController { @@ -15,7 +19,7 @@ class MessageTextFieldController extends TextEditingController { }) : super.fromValue(value); /// A map of style to apply to the text matching the RegExp patterns. - final Map? textPatternStyle; + final Map? textPatternStyle; /// Builds a [TextSpan] from the current text, /// highlighting the matches for [textPatternStyle]. @@ -25,8 +29,14 @@ class MessageTextFieldController extends TextEditingController { TextStyle? style, required bool withComposing, }) { - final pattern = textPatternStyle; - if (pattern == null) { + final pattern = textPatternStyle ?? + { + RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'): + (context) => TextStyle( + color: MessageInputTheme.of(context).linkHighlightColor, + ), + }; + if (pattern.isEmpty) { return super.buildTextSpan( context: context, style: style, @@ -41,7 +51,7 @@ class MessageTextFieldController extends TextEditingController { final key = pattern.keys.firstWhere((it) => it.hasMatch(text)); return TextSpan( text: text, - style: pattern[key], + style: pattern[key]?.call(context), ); }, ); diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index f3818b2e..8a210b96 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -259,6 +259,7 @@ class StreamChatThemeData { sendButtonIdleColor: colorTheme.disabled, inputBackgroundColor: colorTheme.barsBg, inputTextStyle: textTheme.body, + linkHighlightColor: colorTheme.accentPrimary, idleBorderGradient: LinearGradient( colors: [ colorTheme.disabled, diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index 26a995a9..1348af37 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -65,6 +65,7 @@ class MessageInputThemeData with Diagnosticable { this.idleBorderGradient, this.borderRadius, this.expandButtonColor, + this.linkHighlightColor, }); /// Duration of the [MessageInput] send button animation @@ -73,6 +74,9 @@ class MessageInputThemeData with Diagnosticable { /// Background color of [MessageInput] send button final Color? sendButtonColor; + /// Color of a link + final Color? linkHighlightColor; + /// Background color of [MessageInput] action buttons final Color? actionButtonColor; @@ -110,6 +114,7 @@ class MessageInputThemeData with Diagnosticable { Color? actionButtonColor, Color? sendButtonColor, Color? actionButtonIdleColor, + Color? linkHighlightColor, Color? sendButtonIdleColor, Color? expandButtonColor, TextStyle? inputTextStyle, @@ -133,6 +138,7 @@ class MessageInputThemeData with Diagnosticable { activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient, idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, borderRadius: borderRadius ?? this.borderRadius, + linkHighlightColor: linkHighlightColor ?? this.linkHighlightColor, ); /// Linearly interpolate from one [MessageInputThemeData] to another. @@ -161,6 +167,8 @@ class MessageInputThemeData with Diagnosticable { Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t), sendAnimationDuration: a.sendAnimationDuration, inputDecoration: a.inputDecoration, + linkHighlightColor: + Color.lerp(a.linkHighlightColor, b.linkHighlightColor, t), ); /// Merges [this] [MessageInputThemeData] with the [other] @@ -181,6 +189,7 @@ class MessageInputThemeData with Diagnosticable { idleBorderGradient: other.idleBorderGradient, borderRadius: other.borderRadius, expandButtonColor: other.expandButtonColor, + linkHighlightColor: other.linkHighlightColor, ); } @@ -200,7 +209,8 @@ class MessageInputThemeData with Diagnosticable { inputDecoration == other.inputDecoration && idleBorderGradient == other.idleBorderGradient && activeBorderGradient == other.activeBorderGradient && - borderRadius == other.borderRadius; + borderRadius == other.borderRadius && + linkHighlightColor == other.linkHighlightColor; @override int get hashCode => @@ -215,7 +225,8 @@ class MessageInputThemeData with Diagnosticable { inputDecoration.hashCode ^ idleBorderGradient.hashCode ^ activeBorderGradient.hashCode ^ - borderRadius.hashCode; + borderRadius.hashCode ^ + linkHighlightColor.hashCode; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -232,6 +243,7 @@ class MessageInputThemeData with Diagnosticable { ..add(DiagnosticsProperty('activeBorderGradient', activeBorderGradient)) ..add(DiagnosticsProperty('idleBorderGradient', idleBorderGradient)) ..add(DiagnosticsProperty('borderRadius', borderRadius)) - ..add(ColorProperty('expandButtonColor', expandButtonColor)); + ..add(ColorProperty('expandButtonColor', expandButtonColor)) + ..add(ColorProperty('linkHighlightColor', linkHighlightColor)); } } From dbcdd5aea6e59adb006d03c30691f268ee757681 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 3 Jan 2022 09:34:19 +0100 Subject: [PATCH 52/58] fix tests --- .../test/src/core/api/message_api_test.dart | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index e89491a2..ed7fefd6 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -32,6 +32,7 @@ void main() { data: { 'message': message, 'skip_push': false, + 'skip_enrich_url': false, }, )).thenAnswer((_) async => successResponse(path, data: { 'message': message.toJson(), @@ -58,6 +59,7 @@ void main() { data: { 'message': message, 'skip_push': true, + 'skip_enrich_url': false, }, )).thenAnswer((_) async => successResponse(path, data: { 'message': message.toJson(), @@ -137,7 +139,10 @@ void main() { when(() => client.post( path, - data: {'message': message}, + data: { + 'message': message, + 'skip_enrich_url': false, + }, )).thenAnswer( (_) async => successResponse(path, data: {'message': message.toJson()}), ); @@ -162,7 +167,11 @@ void main() { when(() => client.put( path, - data: {'set': set, 'unset': unset}, + data: { + 'set': set, + 'unset': unset, + 'skip_enrich_url': false, + }, )).thenAnswer( (_) async => successResponse(path, data: {'message': message.toJson()}), ); @@ -180,7 +189,11 @@ void main() { verify(() => client.put( path, - data: {'set': set, 'unset': unset}, + data: { + 'set': set, + 'unset': unset, + 'skip_enrich_url': false, + }, )).called(1); verifyNoMoreInteractions(client); }); From 504e72deca272a9a7f941c8bb9aed93448a1ec53 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 3 Jan 2022 16:11:46 +0100 Subject: [PATCH 53/58] add TLD validity check --- .../lib/src/message_input/message_input.dart | 4 +- .../message_text_field_controller.dart | 20 +- .../lib/src/message_input/tld.dart | 1498 +++++++++++++++++ 3 files changed, 1516 insertions(+), 6 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/message_input/tld.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index d0d5e882..180c3d12 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -13,6 +13,7 @@ import 'package:stream_chat_flutter/src/commands_overlay.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/src/emoji_overlay.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/message_input/tld.dart'; import 'package:stream_chat_flutter/src/multi_overlay.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/user_mentions_overlay.dart'; @@ -932,7 +933,8 @@ class MessageInputState extends State if (_lastSearchedContainsUrlText == value) return; _lastSearchedContainsUrlText = value; - final matchedUrls = _urlRegex.allMatches(value); + final matchedUrls = _urlRegex.allMatches(value).toList() + ..removeWhere((it) => it.group(0)?.split('.').last.isValidTLD() == false); // Reset the og attachment if the text doesn't contain any url if (matchedUrls.isEmpty) { diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart index 12ec4831..4a6c3708 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_input/tld.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A function that takes a [BuildContext] and returns a [TextStyle]. -typedef TextStyleBuilder = TextStyle Function(BuildContext context); +typedef TextStyleBuilder = TextStyle? Function( + BuildContext context, + String text, +); /// Controller for the [StreamTextField] widget. class MessageTextFieldController extends TextEditingController { @@ -32,9 +36,12 @@ class MessageTextFieldController extends TextEditingController { final pattern = textPatternStyle ?? { RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'): - (context) => TextStyle( - color: MessageInputTheme.of(context).linkHighlightColor, - ), + (context, text) { + if (!text.split('.').last.isValidTLD()) return null; + return TextStyle( + color: MessageInputTheme.of(context).linkHighlightColor, + ); + }, }; if (pattern.isEmpty) { return super.buildTextSpan( @@ -51,7 +58,10 @@ class MessageTextFieldController extends TextEditingController { final key = pattern.keys.firstWhere((it) => it.hasMatch(text)); return TextSpan( text: text, - style: pattern[key]?.call(context), + style: pattern[key]?.call( + context, + text, + ), ); }, ); diff --git a/packages/stream_chat_flutter/lib/src/message_input/tld.dart b/packages/stream_chat_flutter/lib/src/message_input/tld.dart new file mode 100644 index 00000000..504aa17b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/tld.dart @@ -0,0 +1,1498 @@ +/// Extension on String adding utilities checking TLD validity. +extension TLDString on String { + /// Returns true if the string is a valid TLD. + bool isValidTLD() => tlds.contains(toUpperCase()); +} + +/// List of valid TLDs. +/// https://data.iana.org/TLD/tlds-alpha-by-domain.txt +const tlds = [ + 'AAA', + 'AARP', + 'ABARTH', + 'ABB', + 'ABBOTT', + 'ABBVIE', + 'ABC', + 'ABLE', + 'ABOGADO', + 'ABUDHABI', + 'AC', + 'ACADEMY', + 'ACCENTURE', + 'ACCOUNTANT', + 'ACCOUNTANTS', + 'ACO', + 'ACTOR', + 'AD', + 'ADAC', + 'ADS', + 'ADULT', + 'AE', + 'AEG', + 'AERO', + 'AETNA', + 'AF', + 'AFL', + 'AFRICA', + 'AG', + 'AGAKHAN', + 'AGENCY', + 'AI', + 'AIG', + 'AIRBUS', + 'AIRFORCE', + 'AIRTEL', + 'AKDN', + 'AL', + 'ALFAROMEO', + 'ALIBABA', + 'ALIPAY', + 'ALLFINANZ', + 'ALLSTATE', + 'ALLY', + 'ALSACE', + 'ALSTOM', + 'AM', + 'AMAZON', + 'AMERICANEXPRESS', + 'AMERICANFAMILY', + 'AMEX', + 'AMFAM', + 'AMICA', + 'AMSTERDAM', + 'ANALYTICS', + 'ANDROID', + 'ANQUAN', + 'ANZ', + 'AO', + 'AOL', + 'APARTMENTS', + 'APP', + 'APPLE', + 'AQ', + 'AQUARELLE', + 'AR', + 'ARAB', + 'ARAMCO', + 'ARCHI', + 'ARMY', + 'ARPA', + 'ART', + 'ARTE', + 'AS', + 'ASDA', + 'ASIA', + 'ASSOCIATES', + 'AT', + 'ATHLETA', + 'ATTORNEY', + 'AU', + 'AUCTION', + 'AUDI', + 'AUDIBLE', + 'AUDIO', + 'AUSPOST', + 'AUTHOR', + 'AUTO', + 'AUTOS', + 'AVIANCA', + 'AW', + 'AWS', + 'AX', + 'AXA', + 'AZ', + 'AZURE', + 'BA', + 'BABY', + 'BAIDU', + 'BANAMEX', + 'BANANAREPUBLIC', + 'BAND', + 'BANK', + 'BAR', + 'BARCELONA', + 'BARCLAYCARD', + 'BARCLAYS', + 'BAREFOOT', + 'BARGAINS', + 'BASEBALL', + 'BASKETBALL', + 'BAUHAUS', + 'BAYERN', + 'BB', + 'BBC', + 'BBT', + 'BBVA', + 'BCG', + 'BCN', + 'BD', + 'BE', + 'BEATS', + 'BEAUTY', + 'BEER', + 'BENTLEY', + 'BERLIN', + 'BEST', + 'BESTBUY', + 'BET', + 'BF', + 'BG', + 'BH', + 'BHARTI', + 'BI', + 'BIBLE', + 'BID', + 'BIKE', + 'BING', + 'BINGO', + 'BIO', + 'BIZ', + 'BJ', + 'BLACK', + 'BLACKFRIDAY', + 'BLOCKBUSTER', + 'BLOG', + 'BLOOMBERG', + 'BLUE', + 'BM', + 'BMS', + 'BMW', + 'BN', + 'BNPPARIBAS', + 'BO', + 'BOATS', + 'BOEHRINGER', + 'BOFA', + 'BOM', + 'BOND', + 'BOO', + 'BOOK', + 'BOOKING', + 'BOSCH', + 'BOSTIK', + 'BOSTON', + 'BOT', + 'BOUTIQUE', + 'BOX', + 'BR', + 'BRADESCO', + 'BRIDGESTONE', + 'BROADWAY', + 'BROKER', + 'BROTHER', + 'BRUSSELS', + 'BS', + 'BT', + 'BUDAPEST', + 'BUGATTI', + 'BUILD', + 'BUILDERS', + 'BUSINESS', + 'BUY', + 'BUZZ', + 'BV', + 'BW', + 'BY', + 'BZ', + 'BZH', + 'CA', + 'CAB', + 'CAFE', + 'CAL', + 'CALL', + 'CALVINKLEIN', + 'CAM', + 'CAMERA', + 'CAMP', + 'CANCERRESEARCH', + 'CANON', + 'CAPETOWN', + 'CAPITAL', + 'CAPITALONE', + 'CAR', + 'CARAVAN', + 'CARDS', + 'CARE', + 'CAREER', + 'CAREERS', + 'CARS', + 'CASA', + 'CASE', + 'CASH', + 'CASINO', + 'CAT', + 'CATERING', + 'CATHOLIC', + 'CBA', + 'CBN', + 'CBRE', + 'CBS', + 'CC', + 'CD', + 'CENTER', + 'CEO', + 'CERN', + 'CF', + 'CFA', + 'CFD', + 'CG', + 'CH', + 'CHANEL', + 'CHANNEL', + 'CHARITY', + 'CHASE', + 'CHAT', + 'CHEAP', + 'CHINTAI', + 'CHRISTMAS', + 'CHROME', + 'CHURCH', + 'CI', + 'CIPRIANI', + 'CIRCLE', + 'CISCO', + 'CITADEL', + 'CITI', + 'CITIC', + 'CITY', + 'CITYEATS', + 'CK', + 'CL', + 'CLAIMS', + 'CLEANING', + 'CLICK', + 'CLINIC', + 'CLINIQUE', + 'CLOTHING', + 'CLOUD', + 'CLUB', + 'CLUBMED', + 'CM', + 'CN', + 'CO', + 'COACH', + 'CODES', + 'COFFEE', + 'COLLEGE', + 'COLOGNE', + 'COM', + 'COMCAST', + 'COMMBANK', + 'COMMUNITY', + 'COMPANY', + 'COMPARE', + 'COMPUTER', + 'COMSEC', + 'CONDOS', + 'CONSTRUCTION', + 'CONSULTING', + 'CONTACT', + 'CONTRACTORS', + 'COOKING', + 'COOKINGCHANNEL', + 'COOL', + 'COOP', + 'CORSICA', + 'COUNTRY', + 'COUPON', + 'COUPONS', + 'COURSES', + 'CPA', + 'CR', + 'CREDIT', + 'CREDITCARD', + 'CREDITUNION', + 'CRICKET', + 'CROWN', + 'CRS', + 'CRUISE', + 'CRUISES', + 'CSC', + 'CU', + 'CUISINELLA', + 'CV', + 'CW', + 'CX', + 'CY', + 'CYMRU', + 'CYOU', + 'CZ', + 'DABUR', + 'DAD', + 'DANCE', + 'DATA', + 'DATE', + 'DATING', + 'DATSUN', + 'DAY', + 'DCLK', + 'DDS', + 'DE', + 'DEAL', + 'DEALER', + 'DEALS', + 'DEGREE', + 'DELIVERY', + 'DELL', + 'DELOITTE', + 'DELTA', + 'DEMOCRAT', + 'DENTAL', + 'DENTIST', + 'DESI', + 'DESIGN', + 'DEV', + 'DHL', + 'DIAMONDS', + 'DIET', + 'DIGITAL', + 'DIRECT', + 'DIRECTORY', + 'DISCOUNT', + 'DISCOVER', + 'DISH', + 'DIY', + 'DJ', + 'DK', + 'DM', + 'DNP', + 'DO', + 'DOCS', + 'DOCTOR', + 'DOG', + 'DOMAINS', + 'DOT', + 'DOWNLOAD', + 'DRIVE', + 'DTV', + 'DUBAI', + 'DUNLOP', + 'DUPONT', + 'DURBAN', + 'DVAG', + 'DVR', + 'DZ', + 'EARTH', + 'EAT', + 'EC', + 'ECO', + 'EDEKA', + 'EDU', + 'EDUCATION', + 'EE', + 'EG', + 'EMAIL', + 'EMERCK', + 'ENERGY', + 'ENGINEER', + 'ENGINEERING', + 'ENTERPRISES', + 'EPSON', + 'EQUIPMENT', + 'ER', + 'ERICSSON', + 'ERNI', + 'ES', + 'ESQ', + 'ESTATE', + 'ET', + 'ETISALAT', + 'EU', + 'EUROVISION', + 'EUS', + 'EVENTS', + 'EXCHANGE', + 'EXPERT', + 'EXPOSED', + 'EXPRESS', + 'EXTRASPACE', + 'FAGE', + 'FAIL', + 'FAIRWINDS', + 'FAITH', + 'FAMILY', + 'FAN', + 'FANS', + 'FARM', + 'FARMERS', + 'FASHION', + 'FAST', + 'FEDEX', + 'FEEDBACK', + 'FERRARI', + 'FERRERO', + 'FI', + 'FIAT', + 'FIDELITY', + 'FIDO', + 'FILM', + 'FINAL', + 'FINANCE', + 'FINANCIAL', + 'FIRE', + 'FIRESTONE', + 'FIRMDALE', + 'FISH', + 'FISHING', + 'FIT', + 'FITNESS', + 'FJ', + 'FK', + 'FLICKR', + 'FLIGHTS', + 'FLIR', + 'FLORIST', + 'FLOWERS', + 'FLY', + 'FM', + 'FO', + 'FOO', + 'FOOD', + 'FOODNETWORK', + 'FOOTBALL', + 'FORD', + 'FOREX', + 'FORSALE', + 'FORUM', + 'FOUNDATION', + 'FOX', + 'FR', + 'FREE', + 'FRESENIUS', + 'FRL', + 'FROGANS', + 'FRONTDOOR', + 'FRONTIER', + 'FTR', + 'FUJITSU', + 'FUN', + 'FUND', + 'FURNITURE', + 'FUTBOL', + 'FYI', + 'GA', + 'GAL', + 'GALLERY', + 'GALLO', + 'GALLUP', + 'GAME', + 'GAMES', + 'GAP', + 'GARDEN', + 'GAY', + 'GB', + 'GBIZ', + 'GD', + 'GDN', + 'GE', + 'GEA', + 'GENT', + 'GENTING', + 'GEORGE', + 'GF', + 'GG', + 'GGEE', + 'GH', + 'GI', + 'GIFT', + 'GIFTS', + 'GIVES', + 'GIVING', + 'GL', + 'GLASS', + 'GLE', + 'GLOBAL', + 'GLOBO', + 'GM', + 'GMAIL', + 'GMBH', + 'GMO', + 'GMX', + 'GN', + 'GODADDY', + 'GOLD', + 'GOLDPOINT', + 'GOLF', + 'GOO', + 'GOODYEAR', + 'GOOG', + 'GOOGLE', + 'GOP', + 'GOT', + 'GOV', + 'GP', + 'GQ', + 'GR', + 'GRAINGER', + 'GRAPHICS', + 'GRATIS', + 'GREEN', + 'GRIPE', + 'GROCERY', + 'GROUP', + 'GS', + 'GT', + 'GU', + 'GUARDIAN', + 'GUCCI', + 'GUGE', + 'GUIDE', + 'GUITARS', + 'GURU', + 'GW', + 'GY', + 'HAIR', + 'HAMBURG', + 'HANGOUT', + 'HAUS', + 'HBO', + 'HDFC', + 'HDFCBANK', + 'HEALTH', + 'HEALTHCARE', + 'HELP', + 'HELSINKI', + 'HERE', + 'HERMES', + 'HGTV', + 'HIPHOP', + 'HISAMITSU', + 'HITACHI', + 'HIV', + 'HK', + 'HKT', + 'HM', + 'HN', + 'HOCKEY', + 'HOLDINGS', + 'HOLIDAY', + 'HOMEDEPOT', + 'HOMEGOODS', + 'HOMES', + 'HOMESENSE', + 'HONDA', + 'HORSE', + 'HOSPITAL', + 'HOST', + 'HOSTING', + 'HOT', + 'HOTELES', + 'HOTELS', + 'HOTMAIL', + 'HOUSE', + 'HOW', + 'HR', + 'HSBC', + 'HT', + 'HU', + 'HUGHES', + 'HYATT', + 'HYUNDAI', + 'IBM', + 'ICBC', + 'ICE', + 'ICU', + 'ID', + 'IE', + 'IEEE', + 'IFM', + 'IKANO', + 'IL', + 'IM', + 'IMAMAT', + 'IMDB', + 'IMMO', + 'IMMOBILIEN', + 'IN', + 'INC', + 'INDUSTRIES', + 'INFINITI', + 'INFO', + 'ING', + 'INK', + 'INSTITUTE', + 'INSURANCE', + 'INSURE', + 'INT', + 'INTERNATIONAL', + 'INTUIT', + 'INVESTMENTS', + 'IO', + 'IPIRANGA', + 'IQ', + 'IR', + 'IRISH', + 'IS', + 'ISMAILI', + 'IST', + 'ISTANBUL', + 'IT', + 'ITAU', + 'ITV', + 'JAGUAR', + 'JAVA', + 'JCB', + 'JE', + 'JEEP', + 'JETZT', + 'JEWELRY', + 'JIO', + 'JLL', + 'JM', + 'JMP', + 'JNJ', + 'JO', + 'JOBS', + 'JOBURG', + 'JOT', + 'JOY', + 'JP', + 'JPMORGAN', + 'JPRS', + 'JUEGOS', + 'JUNIPER', + 'KAUFEN', + 'KDDI', + 'KE', + 'KERRYHOTELS', + 'KERRYLOGISTICS', + 'KERRYPROPERTIES', + 'KFH', + 'KG', + 'KH', + 'KI', + 'KIA', + 'KIM', + 'KINDER', + 'KINDLE', + 'KITCHEN', + 'KIWI', + 'KM', + 'KN', + 'KOELN', + 'KOMATSU', + 'KOSHER', + 'KP', + 'KPMG', + 'KPN', + 'KR', + 'KRD', + 'KRED', + 'KUOKGROUP', + 'KW', + 'KY', + 'KYOTO', + 'KZ', + 'LA', + 'LACAIXA', + 'LAMBORGHINI', + 'LAMER', + 'LANCASTER', + 'LANCIA', + 'LAND', + 'LANDROVER', + 'LANXESS', + 'LASALLE', + 'LAT', + 'LATINO', + 'LATROBE', + 'LAW', + 'LAWYER', + 'LB', + 'LC', + 'LDS', + 'LEASE', + 'LECLERC', + 'LEFRAK', + 'LEGAL', + 'LEGO', + 'LEXUS', + 'LGBT', + 'LI', + 'LIDL', + 'LIFE', + 'LIFEINSURANCE', + 'LIFESTYLE', + 'LIGHTING', + 'LIKE', + 'LILLY', + 'LIMITED', + 'LIMO', + 'LINCOLN', + 'LINDE', + 'LINK', + 'LIPSY', + 'LIVE', + 'LIVING', + 'LK', + 'LLC', + 'LLP', + 'LOAN', + 'LOANS', + 'LOCKER', + 'LOCUS', + 'LOFT', + 'LOL', + 'LONDON', + 'LOTTE', + 'LOTTO', + 'LOVE', + 'LPL', + 'LPLFINANCIAL', + 'LR', + 'LS', + 'LT', + 'LTD', + 'LTDA', + 'LU', + 'LUNDBECK', + 'LUXE', + 'LUXURY', + 'LV', + 'LY', + 'MA', + 'MACYS', + 'MADRID', + 'MAIF', + 'MAISON', + 'MAKEUP', + 'MAN', + 'MANAGEMENT', + 'MANGO', + 'MAP', + 'MARKET', + 'MARKETING', + 'MARKETS', + 'MARRIOTT', + 'MARSHALLS', + 'MASERATI', + 'MATTEL', + 'MBA', + 'MC', + 'MCKINSEY', + 'MD', + 'ME', + 'MED', + 'MEDIA', + 'MEET', + 'MELBOURNE', + 'MEME', + 'MEMORIAL', + 'MEN', + 'MENU', + 'MERCKMSD', + 'MG', + 'MH', + 'MIAMI', + 'MICROSOFT', + 'MIL', + 'MINI', + 'MINT', + 'MIT', + 'MITSUBISHI', + 'MK', + 'ML', + 'MLB', + 'MLS', + 'MM', + 'MMA', + 'MN', + 'MO', + 'MOBI', + 'MOBILE', + 'MODA', + 'MOE', + 'MOI', + 'MOM', + 'MONASH', + 'MONEY', + 'MONSTER', + 'MORMON', + 'MORTGAGE', + 'MOSCOW', + 'MOTO', + 'MOTORCYCLES', + 'MOV', + 'MOVIE', + 'MP', + 'MQ', + 'MR', + 'MS', + 'MSD', + 'MT', + 'MTN', + 'MTR', + 'MU', + 'MUSEUM', + 'MUSIC', + 'MUTUAL', + 'MV', + 'MW', + 'MX', + 'MY', + 'MZ', + 'NA', + 'NAB', + 'NAGOYA', + 'NAME', + 'NATURA', + 'NAVY', + 'NBA', + 'NC', + 'NE', + 'NEC', + 'NET', + 'NETBANK', + 'NETFLIX', + 'NETWORK', + 'NEUSTAR', + 'NEW', + 'NEWS', + 'NEXT', + 'NEXTDIRECT', + 'NEXUS', + 'NF', + 'NFL', + 'NG', + 'NGO', + 'NHK', + 'NI', + 'NICO', + 'NIKE', + 'NIKON', + 'NINJA', + 'NISSAN', + 'NISSAY', + 'NL', + 'NO', + 'NOKIA', + 'NORTHWESTERNMUTUAL', + 'NORTON', + 'NOW', + 'NOWRUZ', + 'NOWTV', + 'NP', + 'NR', + 'NRA', + 'NRW', + 'NTT', + 'NU', + 'NYC', + 'NZ', + 'OBI', + 'OBSERVER', + 'OFFICE', + 'OKINAWA', + 'OLAYAN', + 'OLAYANGROUP', + 'OLDNAVY', + 'OLLO', + 'OM', + 'OMEGA', + 'ONE', + 'ONG', + 'ONL', + 'ONLINE', + 'OOO', + 'OPEN', + 'ORACLE', + 'ORANGE', + 'ORG', + 'ORGANIC', + 'ORIGINS', + 'OSAKA', + 'OTSUKA', + 'OTT', + 'OVH', + 'PA', + 'PAGE', + 'PANASONIC', + 'PARIS', + 'PARS', + 'PARTNERS', + 'PARTS', + 'PARTY', + 'PASSAGENS', + 'PAY', + 'PCCW', + 'PE', + 'PET', + 'PF', + 'PFIZER', + 'PG', + 'PH', + 'PHARMACY', + 'PHD', + 'PHILIPS', + 'PHONE', + 'PHOTO', + 'PHOTOGRAPHY', + 'PHOTOS', + 'PHYSIO', + 'PICS', + 'PICTET', + 'PICTURES', + 'PID', + 'PIN', + 'PING', + 'PINK', + 'PIONEER', + 'PIZZA', + 'PK', + 'PL', + 'PLACE', + 'PLAY', + 'PLAYSTATION', + 'PLUMBING', + 'PLUS', + 'PM', + 'PN', + 'PNC', + 'POHL', + 'POKER', + 'POLITIE', + 'PORN', + 'POST', + 'PR', + 'PRAMERICA', + 'PRAXI', + 'PRESS', + 'PRIME', + 'PRO', + 'PROD', + 'PRODUCTIONS', + 'PROF', + 'PROGRESSIVE', + 'PROMO', + 'PROPERTIES', + 'PROPERTY', + 'PROTECTION', + 'PRU', + 'PRUDENTIAL', + 'PS', + 'PT', + 'PUB', + 'PW', + 'PWC', + 'PY', + 'QA', + 'QPON', + 'QUEBEC', + 'QUEST', + 'RACING', + 'RADIO', + 'RE', + 'READ', + 'REALESTATE', + 'REALTOR', + 'REALTY', + 'RECIPES', + 'RED', + 'REDSTONE', + 'REDUMBRELLA', + 'REHAB', + 'REISE', + 'REISEN', + 'REIT', + 'RELIANCE', + 'REN', + 'RENT', + 'RENTALS', + 'REPAIR', + 'REPORT', + 'REPUBLICAN', + 'REST', + 'RESTAURANT', + 'REVIEW', + 'REVIEWS', + 'REXROTH', + 'RICH', + 'RICHARDLI', + 'RICOH', + 'RIL', + 'RIO', + 'RIP', + 'RO', + 'ROCHER', + 'ROCKS', + 'RODEO', + 'ROGERS', + 'ROOM', + 'RS', + 'RSVP', + 'RU', + 'RUGBY', + 'RUHR', + 'RUN', + 'RW', + 'RWE', + 'RYUKYU', + 'SA', + 'SAARLAND', + 'SAFE', + 'SAFETY', + 'SAKURA', + 'SALE', + 'SALON', + 'SAMSCLUB', + 'SAMSUNG', + 'SANDVIK', + 'SANDVIKCOROMANT', + 'SANOFI', + 'SAP', + 'SARL', + 'SAS', + 'SAVE', + 'SAXO', + 'SB', + 'SBI', + 'SBS', + 'SC', + 'SCA', + 'SCB', + 'SCHAEFFLER', + 'SCHMIDT', + 'SCHOLARSHIPS', + 'SCHOOL', + 'SCHULE', + 'SCHWARZ', + 'SCIENCE', + 'SCOT', + 'SD', + 'SE', + 'SEARCH', + 'SEAT', + 'SECURE', + 'SECURITY', + 'SEEK', + 'SELECT', + 'SENER', + 'SERVICES', + 'SES', + 'SEVEN', + 'SEW', + 'SEX', + 'SEXY', + 'SFR', + 'SG', + 'SH', + 'SHANGRILA', + 'SHARP', + 'SHAW', + 'SHELL', + 'SHIA', + 'SHIKSHA', + 'SHOES', + 'SHOP', + 'SHOPPING', + 'SHOUJI', + 'SHOW', + 'SHOWTIME', + 'SI', + 'SILK', + 'SINA', + 'SINGLES', + 'SITE', + 'SJ', + 'SK', + 'SKI', + 'SKIN', + 'SKY', + 'SKYPE', + 'SL', + 'SLING', + 'SM', + 'SMART', + 'SMILE', + 'SN', + 'SNCF', + 'SO', + 'SOCCER', + 'SOCIAL', + 'SOFTBANK', + 'SOFTWARE', + 'SOHU', + 'SOLAR', + 'SOLUTIONS', + 'SONG', + 'SONY', + 'SOY', + 'SPA', + 'SPACE', + 'SPORT', + 'SPOT', + 'SR', + 'SRL', + 'SS', + 'ST', + 'STADA', + 'STAPLES', + 'STAR', + 'STATEBANK', + 'STATEFARM', + 'STC', + 'STCGROUP', + 'STOCKHOLM', + 'STORAGE', + 'STORE', + 'STREAM', + 'STUDIO', + 'STUDY', + 'STYLE', + 'SU', + 'SUCKS', + 'SUPPLIES', + 'SUPPLY', + 'SUPPORT', + 'SURF', + 'SURGERY', + 'SUZUKI', + 'SV', + 'SWATCH', + 'SWISS', + 'SX', + 'SY', + 'SYDNEY', + 'SYSTEMS', + 'SZ', + 'TAB', + 'TAIPEI', + 'TALK', + 'TAOBAO', + 'TARGET', + 'TATAMOTORS', + 'TATAR', + 'TATTOO', + 'TAX', + 'TAXI', + 'TC', + 'TCI', + 'TD', + 'TDK', + 'TEAM', + 'TECH', + 'TECHNOLOGY', + 'TEL', + 'TEMASEK', + 'TENNIS', + 'TEVA', + 'TF', + 'TG', + 'TH', + 'THD', + 'THEATER', + 'THEATRE', + 'TIAA', + 'TICKETS', + 'TIENDA', + 'TIFFANY', + 'TIPS', + 'TIRES', + 'TIROL', + 'TJ', + 'TJMAXX', + 'TJX', + 'TK', + 'TKMAXX', + 'TL', + 'TM', + 'TMALL', + 'TN', + 'TO', + 'TODAY', + 'TOKYO', + 'TOOLS', + 'TOP', + 'TORAY', + 'TOSHIBA', + 'TOTAL', + 'TOURS', + 'TOWN', + 'TOYOTA', + 'TOYS', + 'TR', + 'TRADE', + 'TRADING', + 'TRAINING', + 'TRAVEL', + 'TRAVELCHANNEL', + 'TRAVELERS', + 'TRAVELERSINSURANCE', + 'TRUST', + 'TRV', + 'TT', + 'TUBE', + 'TUI', + 'TUNES', + 'TUSHU', + 'TV', + 'TVS', + 'TW', + 'TZ', + 'UA', + 'UBANK', + 'UBS', + 'UG', + 'UK', + 'UNICOM', + 'UNIVERSITY', + 'UNO', + 'UOL', + 'UPS', + 'US', + 'UY', + 'UZ', + 'VA', + 'VACATIONS', + 'VANA', + 'VANGUARD', + 'VC', + 'VE', + 'VEGAS', + 'VENTURES', + 'VERISIGN', + 'VERSICHERUNG', + 'VET', + 'VG', + 'VI', + 'VIAJES', + 'VIDEO', + 'VIG', + 'VIKING', + 'VILLAS', + 'VIN', + 'VIP', + 'VIRGIN', + 'VISA', + 'VISION', + 'VIVA', + 'VIVO', + 'VLAANDEREN', + 'VN', + 'VODKA', + 'VOLKSWAGEN', + 'VOLVO', + 'VOTE', + 'VOTING', + 'VOTO', + 'VOYAGE', + 'VU', + 'VUELOS', + 'WALES', + 'WALMART', + 'WALTER', + 'WANG', + 'WANGGOU', + 'WATCH', + 'WATCHES', + 'WEATHER', + 'WEATHERCHANNEL', + 'WEBCAM', + 'WEBER', + 'WEBSITE', + 'WED', + 'WEDDING', + 'WEIBO', + 'WEIR', + 'WF', + 'WHOSWHO', + 'WIEN', + 'WIKI', + 'WILLIAMHILL', + 'WIN', + 'WINDOWS', + 'WINE', + 'WINNERS', + 'WME', + 'WOLTERSKLUWER', + 'WOODSIDE', + 'WORK', + 'WORKS', + 'WORLD', + 'WOW', + 'WS', + 'WTC', + 'WTF', + 'XBOX', + 'XEROX', + 'XFINITY', + 'XIHUAN', + 'XIN', + 'XN--11B4C3D', + 'XN--1CK2E1B', + 'XN--1QQW23A', + 'XN--2SCRJ9C', + 'XN--30RR7Y', + 'XN--3BST00M', + 'XN--3DS443G', + 'XN--3E0B707E', + 'XN--3HCRJ9C', + 'XN--3PXU8K', + 'XN--42C2D9A', + 'XN--45BR5CYL', + 'XN--45BRJ9C', + 'XN--45Q11C', + 'XN--4DBRK0CE', + 'XN--4GBRIM', + 'XN--54B7FTA0CC', + 'XN--55QW42G', + 'XN--55QX5D', + 'XN--5SU34J936BGSG', + 'XN--5TZM5G', + 'XN--6FRZ82G', + 'XN--6QQ986B3XL', + 'XN--80ADXHKS', + 'XN--80AO21A', + 'XN--80AQECDR1A', + 'XN--80ASEHDB', + 'XN--80ASWG', + 'XN--8Y0A063A', + 'XN--90A3AC', + 'XN--90AE', + 'XN--90AIS', + 'XN--9DBQ2A', + 'XN--9ET52U', + 'XN--9KRT00A', + 'XN--B4W605FERD', + 'XN--BCK1B9A5DRE4C', + 'XN--C1AVG', + 'XN--C2BR7G', + 'XN--CCK2B3B', + 'XN--CCKWCXETD', + 'XN--CG4BKI', + 'XN--CLCHC0EA0B2G2A9GCD', + 'XN--CZR694B', + 'XN--CZRS0T', + 'XN--CZRU2D', + 'XN--D1ACJ3B', + 'XN--D1ALF', + 'XN--E1A4C', + 'XN--ECKVDTC9D', + 'XN--EFVY88H', + 'XN--FCT429K', + 'XN--FHBEI', + 'XN--FIQ228C5HS', + 'XN--FIQ64B', + 'XN--FIQS8S', + 'XN--FIQZ9S', + 'XN--FJQ720A', + 'XN--FLW351E', + 'XN--FPCRJ9C3D', + 'XN--FZC2C9E2C', + 'XN--FZYS8D69UVGM', + 'XN--G2XX48C', + 'XN--GCKR3F0F', + 'XN--GECRJ9C', + 'XN--GK3AT1E', + 'XN--H2BREG3EVE', + 'XN--H2BRJ9C', + 'XN--H2BRJ9C8C', + 'XN--HXT814E', + 'XN--I1B6B1A6A2E', + 'XN--IMR513N', + 'XN--IO0A7I', + 'XN--J1AEF', + 'XN--J1AMH', + 'XN--J6W193G', + 'XN--JLQ480N2RG', + 'XN--JLQ61U9W7B', + 'XN--JVR189M', + 'XN--KCRX77D1X4A', + 'XN--KPRW13D', + 'XN--KPRY57D', + 'XN--KPUT3I', + 'XN--L1ACC', + 'XN--LGBBAT1AD8J', + 'XN--MGB9AWBF', + 'XN--MGBA3A3EJT', + 'XN--MGBA3A4F16A', + 'XN--MGBA7C0BBN0A', + 'XN--MGBAAKC7DVF', + 'XN--MGBAAM7A8H', + 'XN--MGBAB2BD', + 'XN--MGBAH1A3HJKRD', + 'XN--MGBAI9AZGQP6J', + 'XN--MGBAYH7GPA', + 'XN--MGBBH1A', + 'XN--MGBBH1A71E', + 'XN--MGBC0A9AZCG', + 'XN--MGBCA7DZDO', + 'XN--MGBCPQ6GPA1A', + 'XN--MGBERP4A5D4AR', + 'XN--MGBGU82A', + 'XN--MGBI4ECEXP', + 'XN--MGBPL2FH', + 'XN--MGBT3DHD', + 'XN--MGBTX2B', + 'XN--MGBX4CD0AB', + 'XN--MIX891F', + 'XN--MK1BU44C', + 'XN--MXTQ1M', + 'XN--NGBC5AZD', + 'XN--NGBE9E0A', + 'XN--NGBRX', + 'XN--NODE', + 'XN--NQV7F', + 'XN--NQV7FS00EMA', + 'XN--NYQY26A', + 'XN--O3CW4H', + 'XN--OGBPF8FL', + 'XN--OTU796D', + 'XN--P1ACF', + 'XN--P1AI', + 'XN--PGBS0DH', + 'XN--PSSY2U', + 'XN--Q7CE6A', + 'XN--Q9JYB4C', + 'XN--QCKA1PMC', + 'XN--QXA6A', + 'XN--QXAM', + 'XN--RHQV96G', + 'XN--ROVU88B', + 'XN--RVC1E0AM3E', + 'XN--S9BRJ9C', + 'XN--SES554G', + 'XN--T60B56A', + 'XN--TCKWE', + 'XN--TIQ49XQYJ', + 'XN--UNUP4Y', + 'XN--VERMGENSBERATER-CTB', + 'XN--VERMGENSBERATUNG-PWB', + 'XN--VHQUV', + 'XN--VUQ861B', + 'XN--W4R85EL8FHU5DNRA', + 'XN--W4RS40L', + 'XN--WGBH1C', + 'XN--WGBL6A', + 'XN--XHQ521B', + 'XN--XKC2AL3HYE2A', + 'XN--XKC2DL3A5EE0H', + 'XN--Y9A3AQ', + 'XN--YFRO4I67O', + 'XN--YGBI2AMMX', + 'XN--ZFR164B', + 'XXX', + 'XYZ', + 'YACHTS', + 'YAHOO', + 'YAMAXUN', + 'YANDEX', + 'YE', + 'YODOBASHI', + 'YOGA', + 'YOKOHAMA', + 'YOU', + 'YOUTUBE', + 'YT', + 'YUN', + 'ZA', + 'ZAPPOS', + 'ZARA', + 'ZERO', + 'ZIP', + 'ZM', + 'ZONE', + 'ZUERICH', + 'ZW', +]; From 631ab689900e48bb5eb50f44b92650b5cd650d38 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 4 Jan 2022 14:37:34 +0530 Subject: [PATCH 54/58] make the tld non-linear-searchy --- .../lib/src/message_input/tld.dart | 3037 +++++++++-------- 1 file changed, 1546 insertions(+), 1491 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/tld.dart b/packages/stream_chat_flutter/lib/src/message_input/tld.dart index 504aa17b..2bfa5257 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/tld.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/tld.dart @@ -1,1498 +1,1553 @@ /// Extension on String adding utilities checking TLD validity. extension TLDString on String { /// Returns true if the string is a valid TLD. - bool isValidTLD() => tlds.contains(toUpperCase()); + bool isValidTLD() => + isNotEmpty && + tlds.containsKey(this[0].toUpperCase()) && + tlds[this[0].toUpperCase()]!.contains(toUpperCase()); } /// List of valid TLDs. /// https://data.iana.org/TLD/tlds-alpha-by-domain.txt -const tlds = [ - 'AAA', - 'AARP', - 'ABARTH', - 'ABB', - 'ABBOTT', - 'ABBVIE', - 'ABC', - 'ABLE', - 'ABOGADO', - 'ABUDHABI', - 'AC', - 'ACADEMY', - 'ACCENTURE', - 'ACCOUNTANT', - 'ACCOUNTANTS', - 'ACO', - 'ACTOR', - 'AD', - 'ADAC', - 'ADS', - 'ADULT', - 'AE', - 'AEG', - 'AERO', - 'AETNA', - 'AF', - 'AFL', - 'AFRICA', - 'AG', - 'AGAKHAN', - 'AGENCY', - 'AI', - 'AIG', - 'AIRBUS', - 'AIRFORCE', - 'AIRTEL', - 'AKDN', - 'AL', - 'ALFAROMEO', - 'ALIBABA', - 'ALIPAY', - 'ALLFINANZ', - 'ALLSTATE', - 'ALLY', - 'ALSACE', - 'ALSTOM', - 'AM', - 'AMAZON', - 'AMERICANEXPRESS', - 'AMERICANFAMILY', - 'AMEX', - 'AMFAM', - 'AMICA', - 'AMSTERDAM', - 'ANALYTICS', - 'ANDROID', - 'ANQUAN', - 'ANZ', - 'AO', - 'AOL', - 'APARTMENTS', - 'APP', - 'APPLE', - 'AQ', - 'AQUARELLE', - 'AR', - 'ARAB', - 'ARAMCO', - 'ARCHI', - 'ARMY', - 'ARPA', - 'ART', - 'ARTE', - 'AS', - 'ASDA', - 'ASIA', - 'ASSOCIATES', - 'AT', - 'ATHLETA', - 'ATTORNEY', - 'AU', - 'AUCTION', - 'AUDI', - 'AUDIBLE', - 'AUDIO', - 'AUSPOST', - 'AUTHOR', - 'AUTO', - 'AUTOS', - 'AVIANCA', - 'AW', - 'AWS', - 'AX', - 'AXA', - 'AZ', - 'AZURE', - 'BA', - 'BABY', - 'BAIDU', - 'BANAMEX', - 'BANANAREPUBLIC', - 'BAND', - 'BANK', - 'BAR', - 'BARCELONA', - 'BARCLAYCARD', - 'BARCLAYS', - 'BAREFOOT', - 'BARGAINS', - 'BASEBALL', - 'BASKETBALL', - 'BAUHAUS', - 'BAYERN', - 'BB', - 'BBC', - 'BBT', - 'BBVA', - 'BCG', - 'BCN', - 'BD', - 'BE', - 'BEATS', - 'BEAUTY', - 'BEER', - 'BENTLEY', - 'BERLIN', - 'BEST', - 'BESTBUY', - 'BET', - 'BF', - 'BG', - 'BH', - 'BHARTI', - 'BI', - 'BIBLE', - 'BID', - 'BIKE', - 'BING', - 'BINGO', - 'BIO', - 'BIZ', - 'BJ', - 'BLACK', - 'BLACKFRIDAY', - 'BLOCKBUSTER', - 'BLOG', - 'BLOOMBERG', - 'BLUE', - 'BM', - 'BMS', - 'BMW', - 'BN', - 'BNPPARIBAS', - 'BO', - 'BOATS', - 'BOEHRINGER', - 'BOFA', - 'BOM', - 'BOND', - 'BOO', - 'BOOK', - 'BOOKING', - 'BOSCH', - 'BOSTIK', - 'BOSTON', - 'BOT', - 'BOUTIQUE', - 'BOX', - 'BR', - 'BRADESCO', - 'BRIDGESTONE', - 'BROADWAY', - 'BROKER', - 'BROTHER', - 'BRUSSELS', - 'BS', - 'BT', - 'BUDAPEST', - 'BUGATTI', - 'BUILD', - 'BUILDERS', - 'BUSINESS', - 'BUY', - 'BUZZ', - 'BV', - 'BW', - 'BY', - 'BZ', - 'BZH', - 'CA', - 'CAB', - 'CAFE', - 'CAL', - 'CALL', - 'CALVINKLEIN', - 'CAM', - 'CAMERA', - 'CAMP', - 'CANCERRESEARCH', - 'CANON', - 'CAPETOWN', - 'CAPITAL', - 'CAPITALONE', - 'CAR', - 'CARAVAN', - 'CARDS', - 'CARE', - 'CAREER', - 'CAREERS', - 'CARS', - 'CASA', - 'CASE', - 'CASH', - 'CASINO', - 'CAT', - 'CATERING', - 'CATHOLIC', - 'CBA', - 'CBN', - 'CBRE', - 'CBS', - 'CC', - 'CD', - 'CENTER', - 'CEO', - 'CERN', - 'CF', - 'CFA', - 'CFD', - 'CG', - 'CH', - 'CHANEL', - 'CHANNEL', - 'CHARITY', - 'CHASE', - 'CHAT', - 'CHEAP', - 'CHINTAI', - 'CHRISTMAS', - 'CHROME', - 'CHURCH', - 'CI', - 'CIPRIANI', - 'CIRCLE', - 'CISCO', - 'CITADEL', - 'CITI', - 'CITIC', - 'CITY', - 'CITYEATS', - 'CK', - 'CL', - 'CLAIMS', - 'CLEANING', - 'CLICK', - 'CLINIC', - 'CLINIQUE', - 'CLOTHING', - 'CLOUD', - 'CLUB', - 'CLUBMED', - 'CM', - 'CN', - 'CO', - 'COACH', - 'CODES', - 'COFFEE', - 'COLLEGE', - 'COLOGNE', - 'COM', - 'COMCAST', - 'COMMBANK', - 'COMMUNITY', - 'COMPANY', - 'COMPARE', - 'COMPUTER', - 'COMSEC', - 'CONDOS', - 'CONSTRUCTION', - 'CONSULTING', - 'CONTACT', - 'CONTRACTORS', - 'COOKING', - 'COOKINGCHANNEL', - 'COOL', - 'COOP', - 'CORSICA', - 'COUNTRY', - 'COUPON', - 'COUPONS', - 'COURSES', - 'CPA', - 'CR', - 'CREDIT', - 'CREDITCARD', - 'CREDITUNION', - 'CRICKET', - 'CROWN', - 'CRS', - 'CRUISE', - 'CRUISES', - 'CSC', - 'CU', - 'CUISINELLA', - 'CV', - 'CW', - 'CX', - 'CY', - 'CYMRU', - 'CYOU', - 'CZ', - 'DABUR', - 'DAD', - 'DANCE', - 'DATA', - 'DATE', - 'DATING', - 'DATSUN', - 'DAY', - 'DCLK', - 'DDS', - 'DE', - 'DEAL', - 'DEALER', - 'DEALS', - 'DEGREE', - 'DELIVERY', - 'DELL', - 'DELOITTE', - 'DELTA', - 'DEMOCRAT', - 'DENTAL', - 'DENTIST', - 'DESI', - 'DESIGN', - 'DEV', - 'DHL', - 'DIAMONDS', - 'DIET', - 'DIGITAL', - 'DIRECT', - 'DIRECTORY', - 'DISCOUNT', - 'DISCOVER', - 'DISH', - 'DIY', - 'DJ', - 'DK', - 'DM', - 'DNP', - 'DO', - 'DOCS', - 'DOCTOR', - 'DOG', - 'DOMAINS', - 'DOT', - 'DOWNLOAD', - 'DRIVE', - 'DTV', - 'DUBAI', - 'DUNLOP', - 'DUPONT', - 'DURBAN', - 'DVAG', - 'DVR', - 'DZ', - 'EARTH', - 'EAT', - 'EC', - 'ECO', - 'EDEKA', - 'EDU', - 'EDUCATION', - 'EE', - 'EG', - 'EMAIL', - 'EMERCK', - 'ENERGY', - 'ENGINEER', - 'ENGINEERING', - 'ENTERPRISES', - 'EPSON', - 'EQUIPMENT', - 'ER', - 'ERICSSON', - 'ERNI', - 'ES', - 'ESQ', - 'ESTATE', - 'ET', - 'ETISALAT', - 'EU', - 'EUROVISION', - 'EUS', - 'EVENTS', - 'EXCHANGE', - 'EXPERT', - 'EXPOSED', - 'EXPRESS', - 'EXTRASPACE', - 'FAGE', - 'FAIL', - 'FAIRWINDS', - 'FAITH', - 'FAMILY', - 'FAN', - 'FANS', - 'FARM', - 'FARMERS', - 'FASHION', - 'FAST', - 'FEDEX', - 'FEEDBACK', - 'FERRARI', - 'FERRERO', - 'FI', - 'FIAT', - 'FIDELITY', - 'FIDO', - 'FILM', - 'FINAL', - 'FINANCE', - 'FINANCIAL', - 'FIRE', - 'FIRESTONE', - 'FIRMDALE', - 'FISH', - 'FISHING', - 'FIT', - 'FITNESS', - 'FJ', - 'FK', - 'FLICKR', - 'FLIGHTS', - 'FLIR', - 'FLORIST', - 'FLOWERS', - 'FLY', - 'FM', - 'FO', - 'FOO', - 'FOOD', - 'FOODNETWORK', - 'FOOTBALL', - 'FORD', - 'FOREX', - 'FORSALE', - 'FORUM', - 'FOUNDATION', - 'FOX', - 'FR', - 'FREE', - 'FRESENIUS', - 'FRL', - 'FROGANS', - 'FRONTDOOR', - 'FRONTIER', - 'FTR', - 'FUJITSU', - 'FUN', - 'FUND', - 'FURNITURE', - 'FUTBOL', - 'FYI', - 'GA', - 'GAL', - 'GALLERY', - 'GALLO', - 'GALLUP', - 'GAME', - 'GAMES', - 'GAP', - 'GARDEN', - 'GAY', - 'GB', - 'GBIZ', - 'GD', - 'GDN', - 'GE', - 'GEA', - 'GENT', - 'GENTING', - 'GEORGE', - 'GF', - 'GG', - 'GGEE', - 'GH', - 'GI', - 'GIFT', - 'GIFTS', - 'GIVES', - 'GIVING', - 'GL', - 'GLASS', - 'GLE', - 'GLOBAL', - 'GLOBO', - 'GM', - 'GMAIL', - 'GMBH', - 'GMO', - 'GMX', - 'GN', - 'GODADDY', - 'GOLD', - 'GOLDPOINT', - 'GOLF', - 'GOO', - 'GOODYEAR', - 'GOOG', - 'GOOGLE', - 'GOP', - 'GOT', - 'GOV', - 'GP', - 'GQ', - 'GR', - 'GRAINGER', - 'GRAPHICS', - 'GRATIS', - 'GREEN', - 'GRIPE', - 'GROCERY', - 'GROUP', - 'GS', - 'GT', - 'GU', - 'GUARDIAN', - 'GUCCI', - 'GUGE', - 'GUIDE', - 'GUITARS', - 'GURU', - 'GW', - 'GY', - 'HAIR', - 'HAMBURG', - 'HANGOUT', - 'HAUS', - 'HBO', - 'HDFC', - 'HDFCBANK', - 'HEALTH', - 'HEALTHCARE', - 'HELP', - 'HELSINKI', - 'HERE', - 'HERMES', - 'HGTV', - 'HIPHOP', - 'HISAMITSU', - 'HITACHI', - 'HIV', - 'HK', - 'HKT', - 'HM', - 'HN', - 'HOCKEY', - 'HOLDINGS', - 'HOLIDAY', - 'HOMEDEPOT', - 'HOMEGOODS', - 'HOMES', - 'HOMESENSE', - 'HONDA', - 'HORSE', - 'HOSPITAL', - 'HOST', - 'HOSTING', - 'HOT', - 'HOTELES', - 'HOTELS', - 'HOTMAIL', - 'HOUSE', - 'HOW', - 'HR', - 'HSBC', - 'HT', - 'HU', - 'HUGHES', - 'HYATT', - 'HYUNDAI', - 'IBM', - 'ICBC', - 'ICE', - 'ICU', - 'ID', - 'IE', - 'IEEE', - 'IFM', - 'IKANO', - 'IL', - 'IM', - 'IMAMAT', - 'IMDB', - 'IMMO', - 'IMMOBILIEN', - 'IN', - 'INC', - 'INDUSTRIES', - 'INFINITI', - 'INFO', - 'ING', - 'INK', - 'INSTITUTE', - 'INSURANCE', - 'INSURE', - 'INT', - 'INTERNATIONAL', - 'INTUIT', - 'INVESTMENTS', - 'IO', - 'IPIRANGA', - 'IQ', - 'IR', - 'IRISH', - 'IS', - 'ISMAILI', - 'IST', - 'ISTANBUL', - 'IT', - 'ITAU', - 'ITV', - 'JAGUAR', - 'JAVA', - 'JCB', - 'JE', - 'JEEP', - 'JETZT', - 'JEWELRY', - 'JIO', - 'JLL', - 'JM', - 'JMP', - 'JNJ', - 'JO', - 'JOBS', - 'JOBURG', - 'JOT', - 'JOY', - 'JP', - 'JPMORGAN', - 'JPRS', - 'JUEGOS', - 'JUNIPER', - 'KAUFEN', - 'KDDI', - 'KE', - 'KERRYHOTELS', - 'KERRYLOGISTICS', - 'KERRYPROPERTIES', - 'KFH', - 'KG', - 'KH', - 'KI', - 'KIA', - 'KIM', - 'KINDER', - 'KINDLE', - 'KITCHEN', - 'KIWI', - 'KM', - 'KN', - 'KOELN', - 'KOMATSU', - 'KOSHER', - 'KP', - 'KPMG', - 'KPN', - 'KR', - 'KRD', - 'KRED', - 'KUOKGROUP', - 'KW', - 'KY', - 'KYOTO', - 'KZ', - 'LA', - 'LACAIXA', - 'LAMBORGHINI', - 'LAMER', - 'LANCASTER', - 'LANCIA', - 'LAND', - 'LANDROVER', - 'LANXESS', - 'LASALLE', - 'LAT', - 'LATINO', - 'LATROBE', - 'LAW', - 'LAWYER', - 'LB', - 'LC', - 'LDS', - 'LEASE', - 'LECLERC', - 'LEFRAK', - 'LEGAL', - 'LEGO', - 'LEXUS', - 'LGBT', - 'LI', - 'LIDL', - 'LIFE', - 'LIFEINSURANCE', - 'LIFESTYLE', - 'LIGHTING', - 'LIKE', - 'LILLY', - 'LIMITED', - 'LIMO', - 'LINCOLN', - 'LINDE', - 'LINK', - 'LIPSY', - 'LIVE', - 'LIVING', - 'LK', - 'LLC', - 'LLP', - 'LOAN', - 'LOANS', - 'LOCKER', - 'LOCUS', - 'LOFT', - 'LOL', - 'LONDON', - 'LOTTE', - 'LOTTO', - 'LOVE', - 'LPL', - 'LPLFINANCIAL', - 'LR', - 'LS', - 'LT', - 'LTD', - 'LTDA', - 'LU', - 'LUNDBECK', - 'LUXE', - 'LUXURY', - 'LV', - 'LY', - 'MA', - 'MACYS', - 'MADRID', - 'MAIF', - 'MAISON', - 'MAKEUP', - 'MAN', - 'MANAGEMENT', - 'MANGO', - 'MAP', - 'MARKET', - 'MARKETING', - 'MARKETS', - 'MARRIOTT', - 'MARSHALLS', - 'MASERATI', - 'MATTEL', - 'MBA', - 'MC', - 'MCKINSEY', - 'MD', - 'ME', - 'MED', - 'MEDIA', - 'MEET', - 'MELBOURNE', - 'MEME', - 'MEMORIAL', - 'MEN', - 'MENU', - 'MERCKMSD', - 'MG', - 'MH', - 'MIAMI', - 'MICROSOFT', - 'MIL', - 'MINI', - 'MINT', - 'MIT', - 'MITSUBISHI', - 'MK', - 'ML', - 'MLB', - 'MLS', - 'MM', - 'MMA', - 'MN', - 'MO', - 'MOBI', - 'MOBILE', - 'MODA', - 'MOE', - 'MOI', - 'MOM', - 'MONASH', - 'MONEY', - 'MONSTER', - 'MORMON', - 'MORTGAGE', - 'MOSCOW', - 'MOTO', - 'MOTORCYCLES', - 'MOV', - 'MOVIE', - 'MP', - 'MQ', - 'MR', - 'MS', - 'MSD', - 'MT', - 'MTN', - 'MTR', - 'MU', - 'MUSEUM', - 'MUSIC', - 'MUTUAL', - 'MV', - 'MW', - 'MX', - 'MY', - 'MZ', - 'NA', - 'NAB', - 'NAGOYA', - 'NAME', - 'NATURA', - 'NAVY', - 'NBA', - 'NC', - 'NE', - 'NEC', - 'NET', - 'NETBANK', - 'NETFLIX', - 'NETWORK', - 'NEUSTAR', - 'NEW', - 'NEWS', - 'NEXT', - 'NEXTDIRECT', - 'NEXUS', - 'NF', - 'NFL', - 'NG', - 'NGO', - 'NHK', - 'NI', - 'NICO', - 'NIKE', - 'NIKON', - 'NINJA', - 'NISSAN', - 'NISSAY', - 'NL', - 'NO', - 'NOKIA', - 'NORTHWESTERNMUTUAL', - 'NORTON', - 'NOW', - 'NOWRUZ', - 'NOWTV', - 'NP', - 'NR', - 'NRA', - 'NRW', - 'NTT', - 'NU', - 'NYC', - 'NZ', - 'OBI', - 'OBSERVER', - 'OFFICE', - 'OKINAWA', - 'OLAYAN', - 'OLAYANGROUP', - 'OLDNAVY', - 'OLLO', - 'OM', - 'OMEGA', - 'ONE', - 'ONG', - 'ONL', - 'ONLINE', - 'OOO', - 'OPEN', - 'ORACLE', - 'ORANGE', - 'ORG', - 'ORGANIC', - 'ORIGINS', - 'OSAKA', - 'OTSUKA', - 'OTT', - 'OVH', - 'PA', - 'PAGE', - 'PANASONIC', - 'PARIS', - 'PARS', - 'PARTNERS', - 'PARTS', - 'PARTY', - 'PASSAGENS', - 'PAY', - 'PCCW', - 'PE', - 'PET', - 'PF', - 'PFIZER', - 'PG', - 'PH', - 'PHARMACY', - 'PHD', - 'PHILIPS', - 'PHONE', - 'PHOTO', - 'PHOTOGRAPHY', - 'PHOTOS', - 'PHYSIO', - 'PICS', - 'PICTET', - 'PICTURES', - 'PID', - 'PIN', - 'PING', - 'PINK', - 'PIONEER', - 'PIZZA', - 'PK', - 'PL', - 'PLACE', - 'PLAY', - 'PLAYSTATION', - 'PLUMBING', - 'PLUS', - 'PM', - 'PN', - 'PNC', - 'POHL', - 'POKER', - 'POLITIE', - 'PORN', - 'POST', - 'PR', - 'PRAMERICA', - 'PRAXI', - 'PRESS', - 'PRIME', - 'PRO', - 'PROD', - 'PRODUCTIONS', - 'PROF', - 'PROGRESSIVE', - 'PROMO', - 'PROPERTIES', - 'PROPERTY', - 'PROTECTION', - 'PRU', - 'PRUDENTIAL', - 'PS', - 'PT', - 'PUB', - 'PW', - 'PWC', - 'PY', - 'QA', - 'QPON', - 'QUEBEC', - 'QUEST', - 'RACING', - 'RADIO', - 'RE', - 'READ', - 'REALESTATE', - 'REALTOR', - 'REALTY', - 'RECIPES', - 'RED', - 'REDSTONE', - 'REDUMBRELLA', - 'REHAB', - 'REISE', - 'REISEN', - 'REIT', - 'RELIANCE', - 'REN', - 'RENT', - 'RENTALS', - 'REPAIR', - 'REPORT', - 'REPUBLICAN', - 'REST', - 'RESTAURANT', - 'REVIEW', - 'REVIEWS', - 'REXROTH', - 'RICH', - 'RICHARDLI', - 'RICOH', - 'RIL', - 'RIO', - 'RIP', - 'RO', - 'ROCHER', - 'ROCKS', - 'RODEO', - 'ROGERS', - 'ROOM', - 'RS', - 'RSVP', - 'RU', - 'RUGBY', - 'RUHR', - 'RUN', - 'RW', - 'RWE', - 'RYUKYU', - 'SA', - 'SAARLAND', - 'SAFE', - 'SAFETY', - 'SAKURA', - 'SALE', - 'SALON', - 'SAMSCLUB', - 'SAMSUNG', - 'SANDVIK', - 'SANDVIKCOROMANT', - 'SANOFI', - 'SAP', - 'SARL', - 'SAS', - 'SAVE', - 'SAXO', - 'SB', - 'SBI', - 'SBS', - 'SC', - 'SCA', - 'SCB', - 'SCHAEFFLER', - 'SCHMIDT', - 'SCHOLARSHIPS', - 'SCHOOL', - 'SCHULE', - 'SCHWARZ', - 'SCIENCE', - 'SCOT', - 'SD', - 'SE', - 'SEARCH', - 'SEAT', - 'SECURE', - 'SECURITY', - 'SEEK', - 'SELECT', - 'SENER', - 'SERVICES', - 'SES', - 'SEVEN', - 'SEW', - 'SEX', - 'SEXY', - 'SFR', - 'SG', - 'SH', - 'SHANGRILA', - 'SHARP', - 'SHAW', - 'SHELL', - 'SHIA', - 'SHIKSHA', - 'SHOES', - 'SHOP', - 'SHOPPING', - 'SHOUJI', - 'SHOW', - 'SHOWTIME', - 'SI', - 'SILK', - 'SINA', - 'SINGLES', - 'SITE', - 'SJ', - 'SK', - 'SKI', - 'SKIN', - 'SKY', - 'SKYPE', - 'SL', - 'SLING', - 'SM', - 'SMART', - 'SMILE', - 'SN', - 'SNCF', - 'SO', - 'SOCCER', - 'SOCIAL', - 'SOFTBANK', - 'SOFTWARE', - 'SOHU', - 'SOLAR', - 'SOLUTIONS', - 'SONG', - 'SONY', - 'SOY', - 'SPA', - 'SPACE', - 'SPORT', - 'SPOT', - 'SR', - 'SRL', - 'SS', - 'ST', - 'STADA', - 'STAPLES', - 'STAR', - 'STATEBANK', - 'STATEFARM', - 'STC', - 'STCGROUP', - 'STOCKHOLM', - 'STORAGE', - 'STORE', - 'STREAM', - 'STUDIO', - 'STUDY', - 'STYLE', - 'SU', - 'SUCKS', - 'SUPPLIES', - 'SUPPLY', - 'SUPPORT', - 'SURF', - 'SURGERY', - 'SUZUKI', - 'SV', - 'SWATCH', - 'SWISS', - 'SX', - 'SY', - 'SYDNEY', - 'SYSTEMS', - 'SZ', - 'TAB', - 'TAIPEI', - 'TALK', - 'TAOBAO', - 'TARGET', - 'TATAMOTORS', - 'TATAR', - 'TATTOO', - 'TAX', - 'TAXI', - 'TC', - 'TCI', - 'TD', - 'TDK', - 'TEAM', - 'TECH', - 'TECHNOLOGY', - 'TEL', - 'TEMASEK', - 'TENNIS', - 'TEVA', - 'TF', - 'TG', - 'TH', - 'THD', - 'THEATER', - 'THEATRE', - 'TIAA', - 'TICKETS', - 'TIENDA', - 'TIFFANY', - 'TIPS', - 'TIRES', - 'TIROL', - 'TJ', - 'TJMAXX', - 'TJX', - 'TK', - 'TKMAXX', - 'TL', - 'TM', - 'TMALL', - 'TN', - 'TO', - 'TODAY', - 'TOKYO', - 'TOOLS', - 'TOP', - 'TORAY', - 'TOSHIBA', - 'TOTAL', - 'TOURS', - 'TOWN', - 'TOYOTA', - 'TOYS', - 'TR', - 'TRADE', - 'TRADING', - 'TRAINING', - 'TRAVEL', - 'TRAVELCHANNEL', - 'TRAVELERS', - 'TRAVELERSINSURANCE', - 'TRUST', - 'TRV', - 'TT', - 'TUBE', - 'TUI', - 'TUNES', - 'TUSHU', - 'TV', - 'TVS', - 'TW', - 'TZ', - 'UA', - 'UBANK', - 'UBS', - 'UG', - 'UK', - 'UNICOM', - 'UNIVERSITY', - 'UNO', - 'UOL', - 'UPS', - 'US', - 'UY', - 'UZ', - 'VA', - 'VACATIONS', - 'VANA', - 'VANGUARD', - 'VC', - 'VE', - 'VEGAS', - 'VENTURES', - 'VERISIGN', - 'VERSICHERUNG', - 'VET', - 'VG', - 'VI', - 'VIAJES', - 'VIDEO', - 'VIG', - 'VIKING', - 'VILLAS', - 'VIN', - 'VIP', - 'VIRGIN', - 'VISA', - 'VISION', - 'VIVA', - 'VIVO', - 'VLAANDEREN', - 'VN', - 'VODKA', - 'VOLKSWAGEN', - 'VOLVO', - 'VOTE', - 'VOTING', - 'VOTO', - 'VOYAGE', - 'VU', - 'VUELOS', - 'WALES', - 'WALMART', - 'WALTER', - 'WANG', - 'WANGGOU', - 'WATCH', - 'WATCHES', - 'WEATHER', - 'WEATHERCHANNEL', - 'WEBCAM', - 'WEBER', - 'WEBSITE', - 'WED', - 'WEDDING', - 'WEIBO', - 'WEIR', - 'WF', - 'WHOSWHO', - 'WIEN', - 'WIKI', - 'WILLIAMHILL', - 'WIN', - 'WINDOWS', - 'WINE', - 'WINNERS', - 'WME', - 'WOLTERSKLUWER', - 'WOODSIDE', - 'WORK', - 'WORKS', - 'WORLD', - 'WOW', - 'WS', - 'WTC', - 'WTF', - 'XBOX', - 'XEROX', - 'XFINITY', - 'XIHUAN', - 'XIN', - 'XN--11B4C3D', - 'XN--1CK2E1B', - 'XN--1QQW23A', - 'XN--2SCRJ9C', - 'XN--30RR7Y', - 'XN--3BST00M', - 'XN--3DS443G', - 'XN--3E0B707E', - 'XN--3HCRJ9C', - 'XN--3PXU8K', - 'XN--42C2D9A', - 'XN--45BR5CYL', - 'XN--45BRJ9C', - 'XN--45Q11C', - 'XN--4DBRK0CE', - 'XN--4GBRIM', - 'XN--54B7FTA0CC', - 'XN--55QW42G', - 'XN--55QX5D', - 'XN--5SU34J936BGSG', - 'XN--5TZM5G', - 'XN--6FRZ82G', - 'XN--6QQ986B3XL', - 'XN--80ADXHKS', - 'XN--80AO21A', - 'XN--80AQECDR1A', - 'XN--80ASEHDB', - 'XN--80ASWG', - 'XN--8Y0A063A', - 'XN--90A3AC', - 'XN--90AE', - 'XN--90AIS', - 'XN--9DBQ2A', - 'XN--9ET52U', - 'XN--9KRT00A', - 'XN--B4W605FERD', - 'XN--BCK1B9A5DRE4C', - 'XN--C1AVG', - 'XN--C2BR7G', - 'XN--CCK2B3B', - 'XN--CCKWCXETD', - 'XN--CG4BKI', - 'XN--CLCHC0EA0B2G2A9GCD', - 'XN--CZR694B', - 'XN--CZRS0T', - 'XN--CZRU2D', - 'XN--D1ACJ3B', - 'XN--D1ALF', - 'XN--E1A4C', - 'XN--ECKVDTC9D', - 'XN--EFVY88H', - 'XN--FCT429K', - 'XN--FHBEI', - 'XN--FIQ228C5HS', - 'XN--FIQ64B', - 'XN--FIQS8S', - 'XN--FIQZ9S', - 'XN--FJQ720A', - 'XN--FLW351E', - 'XN--FPCRJ9C3D', - 'XN--FZC2C9E2C', - 'XN--FZYS8D69UVGM', - 'XN--G2XX48C', - 'XN--GCKR3F0F', - 'XN--GECRJ9C', - 'XN--GK3AT1E', - 'XN--H2BREG3EVE', - 'XN--H2BRJ9C', - 'XN--H2BRJ9C8C', - 'XN--HXT814E', - 'XN--I1B6B1A6A2E', - 'XN--IMR513N', - 'XN--IO0A7I', - 'XN--J1AEF', - 'XN--J1AMH', - 'XN--J6W193G', - 'XN--JLQ480N2RG', - 'XN--JLQ61U9W7B', - 'XN--JVR189M', - 'XN--KCRX77D1X4A', - 'XN--KPRW13D', - 'XN--KPRY57D', - 'XN--KPUT3I', - 'XN--L1ACC', - 'XN--LGBBAT1AD8J', - 'XN--MGB9AWBF', - 'XN--MGBA3A3EJT', - 'XN--MGBA3A4F16A', - 'XN--MGBA7C0BBN0A', - 'XN--MGBAAKC7DVF', - 'XN--MGBAAM7A8H', - 'XN--MGBAB2BD', - 'XN--MGBAH1A3HJKRD', - 'XN--MGBAI9AZGQP6J', - 'XN--MGBAYH7GPA', - 'XN--MGBBH1A', - 'XN--MGBBH1A71E', - 'XN--MGBC0A9AZCG', - 'XN--MGBCA7DZDO', - 'XN--MGBCPQ6GPA1A', - 'XN--MGBERP4A5D4AR', - 'XN--MGBGU82A', - 'XN--MGBI4ECEXP', - 'XN--MGBPL2FH', - 'XN--MGBT3DHD', - 'XN--MGBTX2B', - 'XN--MGBX4CD0AB', - 'XN--MIX891F', - 'XN--MK1BU44C', - 'XN--MXTQ1M', - 'XN--NGBC5AZD', - 'XN--NGBE9E0A', - 'XN--NGBRX', - 'XN--NODE', - 'XN--NQV7F', - 'XN--NQV7FS00EMA', - 'XN--NYQY26A', - 'XN--O3CW4H', - 'XN--OGBPF8FL', - 'XN--OTU796D', - 'XN--P1ACF', - 'XN--P1AI', - 'XN--PGBS0DH', - 'XN--PSSY2U', - 'XN--Q7CE6A', - 'XN--Q9JYB4C', - 'XN--QCKA1PMC', - 'XN--QXA6A', - 'XN--QXAM', - 'XN--RHQV96G', - 'XN--ROVU88B', - 'XN--RVC1E0AM3E', - 'XN--S9BRJ9C', - 'XN--SES554G', - 'XN--T60B56A', - 'XN--TCKWE', - 'XN--TIQ49XQYJ', - 'XN--UNUP4Y', - 'XN--VERMGENSBERATER-CTB', - 'XN--VERMGENSBERATUNG-PWB', - 'XN--VHQUV', - 'XN--VUQ861B', - 'XN--W4R85EL8FHU5DNRA', - 'XN--W4RS40L', - 'XN--WGBH1C', - 'XN--WGBL6A', - 'XN--XHQ521B', - 'XN--XKC2AL3HYE2A', - 'XN--XKC2DL3A5EE0H', - 'XN--Y9A3AQ', - 'XN--YFRO4I67O', - 'XN--YGBI2AMMX', - 'XN--ZFR164B', - 'XXX', - 'XYZ', - 'YACHTS', - 'YAHOO', - 'YAMAXUN', - 'YANDEX', - 'YE', - 'YODOBASHI', - 'YOGA', - 'YOKOHAMA', - 'YOU', - 'YOUTUBE', - 'YT', - 'YUN', - 'ZA', - 'ZAPPOS', - 'ZARA', - 'ZERO', - 'ZIP', - 'ZM', - 'ZONE', - 'ZUERICH', - 'ZW', -]; +const tlds = { + 'A': [ + 'AAA', + 'AARP', + 'ABARTH', + 'ABB', + 'ABBOTT', + 'ABBVIE', + 'ABC', + 'ABLE', + 'ABOGADO', + 'ABUDHABI', + 'AC', + 'ACADEMY', + 'ACCENTURE', + 'ACCOUNTANT', + 'ACCOUNTANTS', + 'ACO', + 'ACTOR', + 'AD', + 'ADAC', + 'ADS', + 'ADULT', + 'AE', + 'AEG', + 'AERO', + 'AETNA', + 'AF', + 'AFL', + 'AFRICA', + 'AG', + 'AGAKHAN', + 'AGENCY', + 'AI', + 'AIG', + 'AIRBUS', + 'AIRFORCE', + 'AIRTEL', + 'AKDN', + 'AL', + 'ALFAROMEO', + 'ALIBABA', + 'ALIPAY', + 'ALLFINANZ', + 'ALLSTATE', + 'ALLY', + 'ALSACE', + 'ALSTOM', + 'AM', + 'AMAZON', + 'AMERICANEXPRESS', + 'AMERICANFAMILY', + 'AMEX', + 'AMFAM', + 'AMICA', + 'AMSTERDAM', + 'ANALYTICS', + 'ANDROID', + 'ANQUAN', + 'ANZ', + 'AO', + 'AOL', + 'APARTMENTS', + 'APP', + 'APPLE', + 'AQ', + 'AQUARELLE', + 'AR', + 'ARAB', + 'ARAMCO', + 'ARCHI', + 'ARMY', + 'ARPA', + 'ART', + 'ARTE', + 'AS', + 'ASDA', + 'ASIA', + 'ASSOCIATES', + 'AT', + 'ATHLETA', + 'ATTORNEY', + 'AU', + 'AUCTION', + 'AUDI', + 'AUDIBLE', + 'AUDIO', + 'AUSPOST', + 'AUTHOR', + 'AUTO', + 'AUTOS', + 'AVIANCA', + 'AW', + 'AWS', + 'AX', + 'AXA', + 'AZ', + 'AZURE', + ], + 'B': [ + 'BA', + 'BABY', + 'BAIDU', + 'BANAMEX', + 'BANANAREPUBLIC', + 'BAND', + 'BANK', + 'BAR', + 'BARCELONA', + 'BARCLAYCARD', + 'BARCLAYS', + 'BAREFOOT', + 'BARGAINS', + 'BASEBALL', + 'BASKETBALL', + 'BAUHAUS', + 'BAYERN', + 'BB', + 'BBC', + 'BBT', + 'BBVA', + 'BCG', + 'BCN', + 'BD', + 'BE', + 'BEATS', + 'BEAUTY', + 'BEER', + 'BENTLEY', + 'BERLIN', + 'BEST', + 'BESTBUY', + 'BET', + 'BF', + 'BG', + 'BH', + 'BHARTI', + 'BI', + 'BIBLE', + 'BID', + 'BIKE', + 'BING', + 'BINGO', + 'BIO', + 'BIZ', + 'BJ', + 'BLACK', + 'BLACKFRIDAY', + 'BLOCKBUSTER', + 'BLOG', + 'BLOOMBERG', + 'BLUE', + 'BM', + 'BMS', + 'BMW', + 'BN', + 'BNPPARIBAS', + 'BO', + 'BOATS', + 'BOEHRINGER', + 'BOFA', + 'BOM', + 'BOND', + 'BOO', + 'BOOK', + 'BOOKING', + 'BOSCH', + 'BOSTIK', + 'BOSTON', + 'BOT', + 'BOUTIQUE', + 'BOX', + 'BR', + 'BRADESCO', + 'BRIDGESTONE', + 'BROADWAY', + 'BROKER', + 'BROTHER', + 'BRUSSELS', + 'BS', + 'BT', + 'BUDAPEST', + 'BUGATTI', + 'BUILD', + 'BUILDERS', + 'BUSINESS', + 'BUY', + 'BUZZ', + 'BV', + 'BW', + 'BY', + 'BZ', + 'BZH', + ], + 'C': [ + 'CA', + 'CAB', + 'CAFE', + 'CAL', + 'CALL', + 'CALVINKLEIN', + 'CAM', + 'CAMERA', + 'CAMP', + 'CANCERRESEARCH', + 'CANON', + 'CAPETOWN', + 'CAPITAL', + 'CAPITALONE', + 'CAR', + 'CARAVAN', + 'CARDS', + 'CARE', + 'CAREER', + 'CAREERS', + 'CARS', + 'CASA', + 'CASE', + 'CASH', + 'CASINO', + 'CAT', + 'CATERING', + 'CATHOLIC', + 'CBA', + 'CBN', + 'CBRE', + 'CBS', + 'CC', + 'CD', + 'CENTER', + 'CEO', + 'CERN', + 'CF', + 'CFA', + 'CFD', + 'CG', + 'CH', + 'CHANEL', + 'CHANNEL', + 'CHARITY', + 'CHASE', + 'CHAT', + 'CHEAP', + 'CHINTAI', + 'CHRISTMAS', + 'CHROME', + 'CHURCH', + 'CI', + 'CIPRIANI', + 'CIRCLE', + 'CISCO', + 'CITADEL', + 'CITI', + 'CITIC', + 'CITY', + 'CITYEATS', + 'CK', + 'CL', + 'CLAIMS', + 'CLEANING', + 'CLICK', + 'CLINIC', + 'CLINIQUE', + 'CLOTHING', + 'CLOUD', + 'CLUB', + 'CLUBMED', + 'CM', + 'CN', + 'CO', + 'COACH', + 'CODES', + 'COFFEE', + 'COLLEGE', + 'COLOGNE', + 'COM', + 'COMCAST', + 'COMMBANK', + 'COMMUNITY', + 'COMPANY', + 'COMPARE', + 'COMPUTER', + 'COMSEC', + 'CONDOS', + 'CONSTRUCTION', + 'CONSULTING', + 'CONTACT', + 'CONTRACTORS', + 'COOKING', + 'COOKINGCHANNEL', + 'COOL', + 'COOP', + 'CORSICA', + 'COUNTRY', + 'COUPON', + 'COUPONS', + 'COURSES', + 'CPA', + 'CR', + 'CREDIT', + 'CREDITCARD', + 'CREDITUNION', + 'CRICKET', + 'CROWN', + 'CRS', + 'CRUISE', + 'CRUISES', + 'CSC', + 'CU', + 'CUISINELLA', + 'CV', + 'CW', + 'CX', + 'CY', + 'CYMRU', + 'CYOU', + 'CZ', + ], + 'D': [ + 'DABUR', + 'DAD', + 'DANCE', + 'DATA', + 'DATE', + 'DATING', + 'DATSUN', + 'DAY', + 'DCLK', + 'DDS', + 'DE', + 'DEAL', + 'DEALER', + 'DEALS', + 'DEGREE', + 'DELIVERY', + 'DELL', + 'DELOITTE', + 'DELTA', + 'DEMOCRAT', + 'DENTAL', + 'DENTIST', + 'DESI', + 'DESIGN', + 'DEV', + 'DHL', + 'DIAMONDS', + 'DIET', + 'DIGITAL', + 'DIRECT', + 'DIRECTORY', + 'DISCOUNT', + 'DISCOVER', + 'DISH', + 'DIY', + 'DJ', + 'DK', + 'DM', + 'DNP', + 'DO', + 'DOCS', + 'DOCTOR', + 'DOG', + 'DOMAINS', + 'DOT', + 'DOWNLOAD', + 'DRIVE', + 'DTV', + 'DUBAI', + 'DUNLOP', + 'DUPONT', + 'DURBAN', + 'DVAG', + 'DVR', + 'DZ', + ], + 'E': [ + 'EARTH', + 'EAT', + 'EC', + 'ECO', + 'EDEKA', + 'EDU', + 'EDUCATION', + 'EE', + 'EG', + 'EMAIL', + 'EMERCK', + 'ENERGY', + 'ENGINEER', + 'ENGINEERING', + 'ENTERPRISES', + 'EPSON', + 'EQUIPMENT', + 'ER', + 'ERICSSON', + 'ERNI', + 'ES', + 'ESQ', + 'ESTATE', + 'ET', + 'ETISALAT', + 'EU', + 'EUROVISION', + 'EUS', + 'EVENTS', + 'EXCHANGE', + 'EXPERT', + 'EXPOSED', + 'EXPRESS', + 'EXTRASPACE', + ], + 'F': [ + 'FAGE', + 'FAIL', + 'FAIRWINDS', + 'FAITH', + 'FAMILY', + 'FAN', + 'FANS', + 'FARM', + 'FARMERS', + 'FASHION', + 'FAST', + 'FEDEX', + 'FEEDBACK', + 'FERRARI', + 'FERRERO', + 'FI', + 'FIAT', + 'FIDELITY', + 'FIDO', + 'FILM', + 'FINAL', + 'FINANCE', + 'FINANCIAL', + 'FIRE', + 'FIRESTONE', + 'FIRMDALE', + 'FISH', + 'FISHING', + 'FIT', + 'FITNESS', + 'FJ', + 'FK', + 'FLICKR', + 'FLIGHTS', + 'FLIR', + 'FLORIST', + 'FLOWERS', + 'FLY', + 'FM', + 'FO', + 'FOO', + 'FOOD', + 'FOODNETWORK', + 'FOOTBALL', + 'FORD', + 'FOREX', + 'FORSALE', + 'FORUM', + 'FOUNDATION', + 'FOX', + 'FR', + 'FREE', + 'FRESENIUS', + 'FRL', + 'FROGANS', + 'FRONTDOOR', + 'FRONTIER', + 'FTR', + 'FUJITSU', + 'FUN', + 'FUND', + 'FURNITURE', + 'FUTBOL', + 'FYI', + ], + 'G': [ + 'GA', + 'GAL', + 'GALLERY', + 'GALLO', + 'GALLUP', + 'GAME', + 'GAMES', + 'GAP', + 'GARDEN', + 'GAY', + 'GB', + 'GBIZ', + 'GD', + 'GDN', + 'GE', + 'GEA', + 'GENT', + 'GENTING', + 'GEORGE', + 'GF', + 'GG', + 'GGEE', + 'GH', + 'GI', + 'GIFT', + 'GIFTS', + 'GIVES', + 'GIVING', + 'GL', + 'GLASS', + 'GLE', + 'GLOBAL', + 'GLOBO', + 'GM', + 'GMAIL', + 'GMBH', + 'GMO', + 'GMX', + 'GN', + 'GODADDY', + 'GOLD', + 'GOLDPOINT', + 'GOLF', + 'GOO', + 'GOODYEAR', + 'GOOG', + 'GOOGLE', + 'GOP', + 'GOT', + 'GOV', + 'GP', + 'GQ', + 'GR', + 'GRAINGER', + 'GRAPHICS', + 'GRATIS', + 'GREEN', + 'GRIPE', + 'GROCERY', + 'GROUP', + 'GS', + 'GT', + 'GU', + 'GUARDIAN', + 'GUCCI', + 'GUGE', + 'GUIDE', + 'GUITARS', + 'GURU', + 'GW', + 'GY', + ], + 'H': [ + 'HAIR', + 'HAMBURG', + 'HANGOUT', + 'HAUS', + 'HBO', + 'HDFC', + 'HDFCBANK', + 'HEALTH', + 'HEALTHCARE', + 'HELP', + 'HELSINKI', + 'HERE', + 'HERMES', + 'HGTV', + 'HIPHOP', + 'HISAMITSU', + 'HITACHI', + 'HIV', + 'HK', + 'HKT', + 'HM', + 'HN', + 'HOCKEY', + 'HOLDINGS', + 'HOLIDAY', + 'HOMEDEPOT', + 'HOMEGOODS', + 'HOMES', + 'HOMESENSE', + 'HONDA', + 'HORSE', + 'HOSPITAL', + 'HOST', + 'HOSTING', + 'HOT', + 'HOTELES', + 'HOTELS', + 'HOTMAIL', + 'HOUSE', + 'HOW', + 'HR', + 'HSBC', + 'HT', + 'HU', + 'HUGHES', + 'HYATT', + 'HYUNDAI', + ], + 'I': [ + 'IBM', + 'ICBC', + 'ICE', + 'ICU', + 'ID', + 'IE', + 'IEEE', + 'IFM', + 'IKANO', + 'IL', + 'IM', + 'IMAMAT', + 'IMDB', + 'IMMO', + 'IMMOBILIEN', + 'IN', + 'INC', + 'INDUSTRIES', + 'INFINITI', + 'INFO', + 'ING', + 'INK', + 'INSTITUTE', + 'INSURANCE', + 'INSURE', + 'INT', + 'INTERNATIONAL', + 'INTUIT', + 'INVESTMENTS', + 'IO', + 'IPIRANGA', + 'IQ', + 'IR', + 'IRISH', + 'IS', + 'ISMAILI', + 'IST', + 'ISTANBUL', + 'IT', + 'ITAU', + 'ITV', + ], + 'J': [ + 'JAGUAR', + 'JAVA', + 'JCB', + 'JE', + 'JEEP', + 'JETZT', + 'JEWELRY', + 'JIO', + 'JLL', + 'JM', + 'JMP', + 'JNJ', + 'JO', + 'JOBS', + 'JOBURG', + 'JOT', + 'JOY', + 'JP', + 'JPMORGAN', + 'JPRS', + 'JUEGOS', + 'JUNIPER', + ], + 'K': [ + 'KAUFEN', + 'KDDI', + 'KE', + 'KERRYHOTELS', + 'KERRYLOGISTICS', + 'KERRYPROPERTIES', + 'KFH', + 'KG', + 'KH', + 'KI', + 'KIA', + 'KIM', + 'KINDER', + 'KINDLE', + 'KITCHEN', + 'KIWI', + 'KM', + 'KN', + 'KOELN', + 'KOMATSU', + 'KOSHER', + 'KP', + 'KPMG', + 'KPN', + 'KR', + 'KRD', + 'KRED', + 'KUOKGROUP', + 'KW', + 'KY', + 'KYOTO', + 'KZ', + ], + 'L': [ + 'LA', + 'LACAIXA', + 'LAMBORGHINI', + 'LAMER', + 'LANCASTER', + 'LANCIA', + 'LAND', + 'LANDROVER', + 'LANXESS', + 'LASALLE', + 'LAT', + 'LATINO', + 'LATROBE', + 'LAW', + 'LAWYER', + 'LB', + 'LC', + 'LDS', + 'LEASE', + 'LECLERC', + 'LEFRAK', + 'LEGAL', + 'LEGO', + 'LEXUS', + 'LGBT', + 'LI', + 'LIDL', + 'LIFE', + 'LIFEINSURANCE', + 'LIFESTYLE', + 'LIGHTING', + 'LIKE', + 'LILLY', + 'LIMITED', + 'LIMO', + 'LINCOLN', + 'LINDE', + 'LINK', + 'LIPSY', + 'LIVE', + 'LIVING', + 'LK', + 'LLC', + 'LLP', + 'LOAN', + 'LOANS', + 'LOCKER', + 'LOCUS', + 'LOFT', + 'LOL', + 'LONDON', + 'LOTTE', + 'LOTTO', + 'LOVE', + 'LPL', + 'LPLFINANCIAL', + 'LR', + 'LS', + 'LT', + 'LTD', + 'LTDA', + 'LU', + 'LUNDBECK', + 'LUXE', + 'LUXURY', + 'LV', + 'LY', + ], + 'M': [ + 'MA', + 'MACYS', + 'MADRID', + 'MAIF', + 'MAISON', + 'MAKEUP', + 'MAN', + 'MANAGEMENT', + 'MANGO', + 'MAP', + 'MARKET', + 'MARKETING', + 'MARKETS', + 'MARRIOTT', + 'MARSHALLS', + 'MASERATI', + 'MATTEL', + 'MBA', + 'MC', + 'MCKINSEY', + 'MD', + 'ME', + 'MED', + 'MEDIA', + 'MEET', + 'MELBOURNE', + 'MEME', + 'MEMORIAL', + 'MEN', + 'MENU', + 'MERCKMSD', + 'MG', + 'MH', + 'MIAMI', + 'MICROSOFT', + 'MIL', + 'MINI', + 'MINT', + 'MIT', + 'MITSUBISHI', + 'MK', + 'ML', + 'MLB', + 'MLS', + 'MM', + 'MMA', + 'MN', + 'MO', + 'MOBI', + 'MOBILE', + 'MODA', + 'MOE', + 'MOI', + 'MOM', + 'MONASH', + 'MONEY', + 'MONSTER', + 'MORMON', + 'MORTGAGE', + 'MOSCOW', + 'MOTO', + 'MOTORCYCLES', + 'MOV', + 'MOVIE', + 'MP', + 'MQ', + 'MR', + 'MS', + 'MSD', + 'MT', + 'MTN', + 'MTR', + 'MU', + 'MUSEUM', + 'MUSIC', + 'MUTUAL', + 'MV', + 'MW', + 'MX', + 'MY', + 'MZ', + ], + 'N': [ + 'NA', + 'NAB', + 'NAGOYA', + 'NAME', + 'NATURA', + 'NAVY', + 'NBA', + 'NC', + 'NE', + 'NEC', + 'NET', + 'NETBANK', + 'NETFLIX', + 'NETWORK', + 'NEUSTAR', + 'NEW', + 'NEWS', + 'NEXT', + 'NEXTDIRECT', + 'NEXUS', + 'NF', + 'NFL', + 'NG', + 'NGO', + 'NHK', + 'NI', + 'NICO', + 'NIKE', + 'NIKON', + 'NINJA', + 'NISSAN', + 'NISSAY', + 'NL', + 'NO', + 'NOKIA', + 'NORTHWESTERNMUTUAL', + 'NORTON', + 'NOW', + 'NOWRUZ', + 'NOWTV', + 'NP', + 'NR', + 'NRA', + 'NRW', + 'NTT', + 'NU', + 'NYC', + 'NZ', + ], + 'O': [ + 'OBI', + 'OBSERVER', + 'OFFICE', + 'OKINAWA', + 'OLAYAN', + 'OLAYANGROUP', + 'OLDNAVY', + 'OLLO', + 'OM', + 'OMEGA', + 'ONE', + 'ONG', + 'ONL', + 'ONLINE', + 'OOO', + 'OPEN', + 'ORACLE', + 'ORANGE', + 'ORG', + 'ORGANIC', + 'ORIGINS', + 'OSAKA', + 'OTSUKA', + 'OTT', + 'OVH', + ], + 'P': [ + 'PA', + 'PAGE', + 'PANASONIC', + 'PARIS', + 'PARS', + 'PARTNERS', + 'PARTS', + 'PARTY', + 'PASSAGENS', + 'PAY', + 'PCCW', + 'PE', + 'PET', + 'PF', + 'PFIZER', + 'PG', + 'PH', + 'PHARMACY', + 'PHD', + 'PHILIPS', + 'PHONE', + 'PHOTO', + 'PHOTOGRAPHY', + 'PHOTOS', + 'PHYSIO', + 'PICS', + 'PICTET', + 'PICTURES', + 'PID', + 'PIN', + 'PING', + 'PINK', + 'PIONEER', + 'PIZZA', + 'PK', + 'PL', + 'PLACE', + 'PLAY', + 'PLAYSTATION', + 'PLUMBING', + 'PLUS', + 'PM', + 'PN', + 'PNC', + 'POHL', + 'POKER', + 'POLITIE', + 'PORN', + 'POST', + 'PR', + 'PRAMERICA', + 'PRAXI', + 'PRESS', + 'PRIME', + 'PRO', + 'PROD', + 'PRODUCTIONS', + 'PROF', + 'PROGRESSIVE', + 'PROMO', + 'PROPERTIES', + 'PROPERTY', + 'PROTECTION', + 'PRU', + 'PRUDENTIAL', + 'PS', + 'PT', + 'PUB', + 'PW', + 'PWC', + 'PY', + ], + 'Q': [ + 'QA', + 'QPON', + 'QUEBEC', + 'QUEST', + ], + 'R': [ + 'RACING', + 'RADIO', + 'RE', + 'READ', + 'REALESTATE', + 'REALTOR', + 'REALTY', + 'RECIPES', + 'RED', + 'REDSTONE', + 'REDUMBRELLA', + 'REHAB', + 'REISE', + 'REISEN', + 'REIT', + 'RELIANCE', + 'REN', + 'RENT', + 'RENTALS', + 'REPAIR', + 'REPORT', + 'REPUBLICAN', + 'REST', + 'RESTAURANT', + 'REVIEW', + 'REVIEWS', + 'REXROTH', + 'RICH', + 'RICHARDLI', + 'RICOH', + 'RIL', + 'RIO', + 'RIP', + 'RO', + 'ROCHER', + 'ROCKS', + 'RODEO', + 'ROGERS', + 'ROOM', + 'RS', + 'RSVP', + 'RU', + 'RUGBY', + 'RUHR', + 'RUN', + 'RW', + 'RWE', + 'RYUKYU', + ], + 'S': [ + 'SA', + 'SAARLAND', + 'SAFE', + 'SAFETY', + 'SAKURA', + 'SALE', + 'SALON', + 'SAMSCLUB', + 'SAMSUNG', + 'SANDVIK', + 'SANDVIKCOROMANT', + 'SANOFI', + 'SAP', + 'SARL', + 'SAS', + 'SAVE', + 'SAXO', + 'SB', + 'SBI', + 'SBS', + 'SC', + 'SCA', + 'SCB', + 'SCHAEFFLER', + 'SCHMIDT', + 'SCHOLARSHIPS', + 'SCHOOL', + 'SCHULE', + 'SCHWARZ', + 'SCIENCE', + 'SCOT', + 'SD', + 'SE', + 'SEARCH', + 'SEAT', + 'SECURE', + 'SECURITY', + 'SEEK', + 'SELECT', + 'SENER', + 'SERVICES', + 'SES', + 'SEVEN', + 'SEW', + 'SEX', + 'SEXY', + 'SFR', + 'SG', + 'SH', + 'SHANGRILA', + 'SHARP', + 'SHAW', + 'SHELL', + 'SHIA', + 'SHIKSHA', + 'SHOES', + 'SHOP', + 'SHOPPING', + 'SHOUJI', + 'SHOW', + 'SHOWTIME', + 'SI', + 'SILK', + 'SINA', + 'SINGLES', + 'SITE', + 'SJ', + 'SK', + 'SKI', + 'SKIN', + 'SKY', + 'SKYPE', + 'SL', + 'SLING', + 'SM', + 'SMART', + 'SMILE', + 'SN', + 'SNCF', + 'SO', + 'SOCCER', + 'SOCIAL', + 'SOFTBANK', + 'SOFTWARE', + 'SOHU', + 'SOLAR', + 'SOLUTIONS', + 'SONG', + 'SONY', + 'SOY', + 'SPA', + 'SPACE', + 'SPORT', + 'SPOT', + 'SR', + 'SRL', + 'SS', + 'ST', + 'STADA', + 'STAPLES', + 'STAR', + 'STATEBANK', + 'STATEFARM', + 'STC', + 'STCGROUP', + 'STOCKHOLM', + 'STORAGE', + 'STORE', + 'STREAM', + 'STUDIO', + 'STUDY', + 'STYLE', + 'SU', + 'SUCKS', + 'SUPPLIES', + 'SUPPLY', + 'SUPPORT', + 'SURF', + 'SURGERY', + 'SUZUKI', + 'SV', + 'SWATCH', + 'SWISS', + 'SX', + 'SY', + 'SYDNEY', + 'SYSTEMS', + 'SZ', + ], + 'T': [ + 'TAB', + 'TAIPEI', + 'TALK', + 'TAOBAO', + 'TARGET', + 'TATAMOTORS', + 'TATAR', + 'TATTOO', + 'TAX', + 'TAXI', + 'TC', + 'TCI', + 'TD', + 'TDK', + 'TEAM', + 'TECH', + 'TECHNOLOGY', + 'TEL', + 'TEMASEK', + 'TENNIS', + 'TEVA', + 'TF', + 'TG', + 'TH', + 'THD', + 'THEATER', + 'THEATRE', + 'TIAA', + 'TICKETS', + 'TIENDA', + 'TIFFANY', + 'TIPS', + 'TIRES', + 'TIROL', + 'TJ', + 'TJMAXX', + 'TJX', + 'TK', + 'TKMAXX', + 'TL', + 'TM', + 'TMALL', + 'TN', + 'TO', + 'TODAY', + 'TOKYO', + 'TOOLS', + 'TOP', + 'TORAY', + 'TOSHIBA', + 'TOTAL', + 'TOURS', + 'TOWN', + 'TOYOTA', + 'TOYS', + 'TR', + 'TRADE', + 'TRADING', + 'TRAINING', + 'TRAVEL', + 'TRAVELCHANNEL', + 'TRAVELERS', + 'TRAVELERSINSURANCE', + 'TRUST', + 'TRV', + 'TT', + 'TUBE', + 'TUI', + 'TUNES', + 'TUSHU', + 'TV', + 'TVS', + 'TW', + 'TZ', + ], + 'U': [ + 'UA', + 'UBANK', + 'UBS', + 'UG', + 'UK', + 'UNICOM', + 'UNIVERSITY', + 'UNO', + 'UOL', + 'UPS', + 'US', + 'UY', + 'UZ', + ], + 'V': [ + 'VA', + 'VACATIONS', + 'VANA', + 'VANGUARD', + 'VC', + 'VE', + 'VEGAS', + 'VENTURES', + 'VERISIGN', + 'VERSICHERUNG', + 'VET', + 'VG', + 'VI', + 'VIAJES', + 'VIDEO', + 'VIG', + 'VIKING', + 'VILLAS', + 'VIN', + 'VIP', + 'VIRGIN', + 'VISA', + 'VISION', + 'VIVA', + 'VIVO', + 'VLAANDEREN', + 'VN', + 'VODKA', + 'VOLKSWAGEN', + 'VOLVO', + 'VOTE', + 'VOTING', + 'VOTO', + 'VOYAGE', + 'VU', + 'VUELOS', + ], + 'W': [ + 'WALES', + 'WALMART', + 'WALTER', + 'WANG', + 'WANGGOU', + 'WATCH', + 'WATCHES', + 'WEATHER', + 'WEATHERCHANNEL', + 'WEBCAM', + 'WEBER', + 'WEBSITE', + 'WED', + 'WEDDING', + 'WEIBO', + 'WEIR', + 'WF', + 'WHOSWHO', + 'WIEN', + 'WIKI', + 'WILLIAMHILL', + 'WIN', + 'WINDOWS', + 'WINE', + 'WINNERS', + 'WME', + 'WOLTERSKLUWER', + 'WOODSIDE', + 'WORK', + 'WORKS', + 'WORLD', + 'WOW', + 'WS', + 'WTC', + 'WTF', + ], + 'X': [ + 'XBOX', + 'XEROX', + 'XFINITY', + 'XIHUAN', + 'XIN', + 'XN--11B4C3D', + 'XN--1CK2E1B', + 'XN--1QQW23A', + 'XN--2SCRJ9C', + 'XN--30RR7Y', + 'XN--3BST00M', + 'XN--3DS443G', + 'XN--3E0B707E', + 'XN--3HCRJ9C', + 'XN--3PXU8K', + 'XN--42C2D9A', + 'XN--45BR5CYL', + 'XN--45BRJ9C', + 'XN--45Q11C', + 'XN--4DBRK0CE', + 'XN--4GBRIM', + 'XN--54B7FTA0CC', + 'XN--55QW42G', + 'XN--55QX5D', + 'XN--5SU34J936BGSG', + 'XN--5TZM5G', + 'XN--6FRZ82G', + 'XN--6QQ986B3XL', + 'XN--80ADXHKS', + 'XN--80AO21A', + 'XN--80AQECDR1A', + 'XN--80ASEHDB', + 'XN--80ASWG', + 'XN--8Y0A063A', + 'XN--90A3AC', + 'XN--90AE', + 'XN--90AIS', + 'XN--9DBQ2A', + 'XN--9ET52U', + 'XN--9KRT00A', + 'XN--B4W605FERD', + 'XN--BCK1B9A5DRE4C', + 'XN--C1AVG', + 'XN--C2BR7G', + 'XN--CCK2B3B', + 'XN--CCKWCXETD', + 'XN--CG4BKI', + 'XN--CLCHC0EA0B2G2A9GCD', + 'XN--CZR694B', + 'XN--CZRS0T', + 'XN--CZRU2D', + 'XN--D1ACJ3B', + 'XN--D1ALF', + 'XN--E1A4C', + 'XN--ECKVDTC9D', + 'XN--EFVY88H', + 'XN--FCT429K', + 'XN--FHBEI', + 'XN--FIQ228C5HS', + 'XN--FIQ64B', + 'XN--FIQS8S', + 'XN--FIQZ9S', + 'XN--FJQ720A', + 'XN--FLW351E', + 'XN--FPCRJ9C3D', + 'XN--FZC2C9E2C', + 'XN--FZYS8D69UVGM', + 'XN--G2XX48C', + 'XN--GCKR3F0F', + 'XN--GECRJ9C', + 'XN--GK3AT1E', + 'XN--H2BREG3EVE', + 'XN--H2BRJ9C', + 'XN--H2BRJ9C8C', + 'XN--HXT814E', + 'XN--I1B6B1A6A2E', + 'XN--IMR513N', + 'XN--IO0A7I', + 'XN--J1AEF', + 'XN--J1AMH', + 'XN--J6W193G', + 'XN--JLQ480N2RG', + 'XN--JLQ61U9W7B', + 'XN--JVR189M', + 'XN--KCRX77D1X4A', + 'XN--KPRW13D', + 'XN--KPRY57D', + 'XN--KPUT3I', + 'XN--L1ACC', + 'XN--LGBBAT1AD8J', + 'XN--MGB9AWBF', + 'XN--MGBA3A3EJT', + 'XN--MGBA3A4F16A', + 'XN--MGBA7C0BBN0A', + 'XN--MGBAAKC7DVF', + 'XN--MGBAAM7A8H', + 'XN--MGBAB2BD', + 'XN--MGBAH1A3HJKRD', + 'XN--MGBAI9AZGQP6J', + 'XN--MGBAYH7GPA', + 'XN--MGBBH1A', + 'XN--MGBBH1A71E', + 'XN--MGBC0A9AZCG', + 'XN--MGBCA7DZDO', + 'XN--MGBCPQ6GPA1A', + 'XN--MGBERP4A5D4AR', + 'XN--MGBGU82A', + 'XN--MGBI4ECEXP', + 'XN--MGBPL2FH', + 'XN--MGBT3DHD', + 'XN--MGBTX2B', + 'XN--MGBX4CD0AB', + 'XN--MIX891F', + 'XN--MK1BU44C', + 'XN--MXTQ1M', + 'XN--NGBC5AZD', + 'XN--NGBE9E0A', + 'XN--NGBRX', + 'XN--NODE', + 'XN--NQV7F', + 'XN--NQV7FS00EMA', + 'XN--NYQY26A', + 'XN--O3CW4H', + 'XN--OGBPF8FL', + 'XN--OTU796D', + 'XN--P1ACF', + 'XN--P1AI', + 'XN--PGBS0DH', + 'XN--PSSY2U', + 'XN--Q7CE6A', + 'XN--Q9JYB4C', + 'XN--QCKA1PMC', + 'XN--QXA6A', + 'XN--QXAM', + 'XN--RHQV96G', + 'XN--ROVU88B', + 'XN--RVC1E0AM3E', + 'XN--S9BRJ9C', + 'XN--SES554G', + 'XN--T60B56A', + 'XN--TCKWE', + 'XN--TIQ49XQYJ', + 'XN--UNUP4Y', + 'XN--VERMGENSBERATER-CTB', + 'XN--VERMGENSBERATUNG-PWB', + 'XN--VHQUV', + 'XN--VUQ861B', + 'XN--W4R85EL8FHU5DNRA', + 'XN--W4RS40L', + 'XN--WGBH1C', + 'XN--WGBL6A', + 'XN--XHQ521B', + 'XN--XKC2AL3HYE2A', + 'XN--XKC2DL3A5EE0H', + 'XN--Y9A3AQ', + 'XN--YFRO4I67O', + 'XN--YGBI2AMMX', + 'XN--ZFR164B', + 'XXX', + 'XYZ', + ], + 'Y': [ + 'YACHTS', + 'YAHOO', + 'YAMAXUN', + 'YANDEX', + 'YE', + 'YODOBASHI', + 'YOGA', + 'YOKOHAMA', + 'YOU', + 'YOUTUBE', + 'YT', + 'YUN', + ], + 'Z': [ + 'ZA', + 'ZAPPOS', + 'ZARA', + 'ZERO', + 'ZIP', + 'ZM', + 'ZONE', + 'ZUERICH', + 'ZW', + ], +}; From fa5e993d299e10e4a52d5360e2f98672e0a3c884 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 4 Jan 2022 15:29:03 +0530 Subject: [PATCH 55/58] tried fix for mip bug --- .../lib/src/message_input/message_input.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 180c3d12..43806c5b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -938,7 +938,9 @@ class MessageInputState extends State // Reset the og attachment if the text doesn't contain any url if (matchedUrls.isEmpty) { - _effectiveController.clearOGAttachment(); + _effectiveController + ..text = value + ..clearOGAttachment(); return; } From dcbdfc32eadd34673cec6383a5ed7975203c7b6b Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 11 Jan 2022 12:48:42 +0530 Subject: [PATCH 56/58] added mip didUpdateWidget for focusnode --- packages/stream_chat_flutter/CHANGELOG.md | 4 ++++ .../lib/src/message_input/message_input.dart | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 258e4cc5..db9fef23 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -12,6 +12,10 @@ - Videos can now be auto-played in `FullScreenMedia` +🔄 Changed + +- Add `didUpdateWidget` override in `MessageInput` widget to handle changes to `focusNode`. + ## 3.3.2 - Updated `stream_chat_flutter_core` dependency to [`3.3.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog). diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index e55fac19..c9443007 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -352,7 +352,7 @@ class MessageInput extends StatefulWidget { class MessageInputState extends State with RestorationMixin { final _imagePicker = ImagePicker(); - late final _focusNode = widget.focusNode ?? FocusNode(); + late var _focusNode = widget.focusNode ?? FocusNode(); bool _inputEnabled = true; bool get _commandEnabled => _effectiveController.value.command != null; bool _showCommandsOverlay = false; @@ -419,6 +419,13 @@ class MessageInputState extends State _controller!.dispose(); _controller = null; } + + // Update _focusNode + if (widget.focusNode != null && oldWidget.focusNode != widget.focusNode) { + _focusNode.removeListener(_focusNodeListener); + _focusNode = widget.focusNode!; + _focusNode.addListener(_focusNodeListener); + } } @override From 8bd826d08d58b98d2766d60d1cf384999e8c9e45 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 11 Jan 2022 12:50:02 +0530 Subject: [PATCH 57/58] change var to focusnode --- .../lib/src/message_input/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index c9443007..54170b4c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -352,7 +352,7 @@ class MessageInput extends StatefulWidget { class MessageInputState extends State with RestorationMixin { final _imagePicker = ImagePicker(); - late var _focusNode = widget.focusNode ?? FocusNode(); + late FocusNode _focusNode = widget.focusNode ?? FocusNode(); bool _inputEnabled = true; bool get _commandEnabled => _effectiveController.value.command != null; bool _showCommandsOverlay = false; From e21d32941adcd4f36323757ae6905b4cc82ad434 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 11 Jan 2022 16:07:11 +0530 Subject: [PATCH 58/58] add listener to effective controller --- .../lib/src/message_input/message_input.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 54170b4c..adcb046a 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -398,11 +398,7 @@ class MessageInputState extends State if (widget.messageInputController == null) { _createLocalController(); } else { - _effectiveController.textEditingController - .removeListener(_onChangedDebounced); - _effectiveController.textEditingController - .addListener(_onChangedDebounced); - if (!_isEditing && _timeOut <= 0) _startSlowMode(); + _initialiseEffectiveController(); } _focusNode.addListener(_focusNodeListener); } @@ -418,6 +414,7 @@ class MessageInputState extends State unregisterFromRestoration(_controller!); _controller!.dispose(); _controller = null; + _initialiseEffectiveController(); } // Update _focusNode @@ -447,6 +444,13 @@ class MessageInputState extends State int _timeOut = 0; Timer? _slowModeTimer; + void _initialiseEffectiveController() { + _effectiveController.textEditingController + .removeListener(_onChangedDebounced); + _effectiveController.textEditingController.addListener(_onChangedDebounced); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); + } + void _startSlowMode() { if (!mounted) { return;