From 011fb1b2d9831f4add1d1b1ae8db2003b9c1573b Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 19 Oct 2021 15:31:20 +0530 Subject: [PATCH 001/112] 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 002/112] 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 003/112] 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 004/112] 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 005/112] 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 006/112] 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 007/112] 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 008/112] 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 009/112] 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 010/112] 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 011/112] 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 012/112] 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 013/112] 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 014/112] 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 015/112] 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 016/112] 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 11b5de6fd733e3640812cc95aefbe2fdad681f83 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 8 Nov 2021 21:29:48 +0530 Subject: [PATCH 017/112] added channel user capabilities --- .../stream_chat/lib/src/core/models/channel_model.dart | 9 +++++++++ .../stream_chat/lib/src/core/models/channel_model.g.dart | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index ed7c2c2e..967fe827 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -13,6 +13,7 @@ class ChannelModel { String? id, String? type, String? cid, + this.ownCapabilities = const [], ChannelConfig? config, this.createdBy, this.frozen = false, @@ -51,6 +52,10 @@ class ChannelModel { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String cid; + /// List of user permissions on this channel + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final List ownCapabilities; + /// The channel configuration data @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final ChannelConfig config; @@ -101,6 +106,7 @@ class ChannelModel { 'id', 'type', 'cid', + 'own_capabilities', 'config', 'created_by', 'frozen', @@ -127,6 +133,7 @@ class ChannelModel { String? id, String? type, String? cid, + List? ownCapabilities, ChannelConfig? config, User? createdBy, bool? frozen, @@ -143,6 +150,7 @@ class ChannelModel { id: id ?? this.id, type: type ?? this.type, cid: cid ?? this.cid, + ownCapabilities: ownCapabilities ?? this.ownCapabilities, config: config ?? this.config, createdBy: createdBy ?? this.createdBy, frozen: frozen ?? this.frozen, @@ -164,6 +172,7 @@ class ChannelModel { id: other.id, type: other.type, cid: other.cid, + ownCapabilities: other.ownCapabilities, config: other.config, createdBy: other.createdBy, frozen: other.frozen, diff --git a/packages/stream_chat/lib/src/core/models/channel_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart index 94d9a81a..974ef383 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.g.dart @@ -10,6 +10,10 @@ ChannelModel _$ChannelModelFromJson(Map json) => ChannelModel( id: json['id'] as String?, type: json['type'] as String?, cid: json['cid'] as String?, + ownCapabilities: (json['own_capabilities'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], config: json['config'] == null ? null : ChannelConfig.fromJson(json['config'] as Map), @@ -48,6 +52,7 @@ Map _$ChannelModelToJson(ChannelModel instance) { } writeNotNull('cid', readonly(instance.cid)); + writeNotNull('own_capabilities', readonly(instance.ownCapabilities)); writeNotNull('config', readonly(instance.config)); writeNotNull('created_by', readonly(instance.createdBy)); val['frozen'] = instance.frozen; From c60524ef96f1a05f3895811c958ff7b2388560c7 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 9 Nov 2021 13:48:12 +0530 Subject: [PATCH 018/112] added permission types --- .../stream_chat/lib/src/permission_type.dart | 37 +++++++++++++++++++ packages/stream_chat/lib/stream_chat.dart | 1 + .../lib/src/message_list_view.dart | 12 +++--- 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 packages/stream_chat/lib/src/permission_type.dart diff --git a/packages/stream_chat/lib/src/permission_type.dart b/packages/stream_chat/lib/src/permission_type.dart new file mode 100644 index 00000000..2e6eef64 --- /dev/null +++ b/packages/stream_chat/lib/src/permission_type.dart @@ -0,0 +1,37 @@ +class PermissionType { + static const String sendMessage = "send-message"; + + static const String sendReaction = "send-reaction"; + + static const String sendLinks = "send-links"; + + static const String sendReply = "send-reply"; + + static const String freezeChannel = "freeze-channel"; + + static const String setChannelCooldown = "set-channel-cooldown"; + + static const String leaveChannel = "leave-channel"; + + static const String pinMessage = "pin-message"; + + static const String deleteAnyMessage = "delete-any-message"; + + static const String deleteOwnMessage = "delete-own-message"; + + static const String updateAnyMessage = "update-any-message"; + + static const String updateOwnMessage = "update-own-message"; + + static const String searchMessages = "search-messages"; + + static const String sendTypingEvents = "send-typing-events"; + + static const String uploadFile = "upload-file"; + + static const String deleteChannel = "delete-channel"; + + static const String updateChannel = "update-channel"; + + static const String updateChannelMembers = "update-channel-members"; +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 571d78f5..d196b675 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -36,6 +36,7 @@ export './src/core/util/extension.dart'; export './src/db/chat_persistence_client.dart'; export './src/event_type.dart'; export './src/location.dart'; +export './src/permission_type.dart'; export './src/ws/connection_status.dart'; export 'src/client/channel.dart'; export 'src/client/client.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 5424a1c2..12902030 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -164,7 +164,6 @@ class MessageListView extends StatefulWidget { this.messageFilter, this.onMessageTap, this.onSystemMessageTap, - this.pinPermissions = const [], this.showFloatingDateDivider = true, this.threadSeparatorBuilder, this.messageListController, @@ -276,9 +275,6 @@ class MessageListView extends StatefulWidget { /// Called when system message is tapped final OnMessageTap? onSystemMessageTap; - /// A List of user types that have permission to pin messages - final List pinPermissions; - /// Builder used to build the thread separator in case it's a thread view final WidgetBuilder? threadSeparatorBuilder; @@ -301,6 +297,7 @@ class _MessageListViewState extends State { int? _messageListLength; StreamChannelState? streamChannel; late StreamChatThemeData _streamTheme; + late List _userPermissions; int get _initialIndex { final initialScrollIndex = widget.initialScrollIndex; @@ -960,7 +957,7 @@ class _MessageListViewState extends State { FocusScope.of(context).unfocus(); }, showPinButton: currentUserMember != null && - widget.pinPermissions.contains(currentUserMember.role), + _userPermissions.contains(PermissionType.pinMessage), ); if (widget.parentMessageBuilder != null) { @@ -1159,7 +1156,7 @@ class _MessageListViewState extends State { FocusScope.of(context).unfocus(); }, showPinButton: currentUserMember != null && - widget.pinPermissions.contains(currentUserMember.role), + _userPermissions.contains(PermissionType.pinMessage), ); if (widget.messageBuilder != null) { @@ -1239,6 +1236,9 @@ class _MessageListViewState extends State { void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); _streamTheme = StreamChatTheme.of(context); + _userPermissions = + newStreamChannel.channel.state?.channelState.channel?.ownCapabilities ?? + []; if (newStreamChannel != streamChannel) { streamChannel = newStreamChannel; From c10a2be6ed55d22b1374694411cc02551eb3e970 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 9 Nov 2021 14:19:28 +0530 Subject: [PATCH 019/112] updated action modal --- .../lib/src/message_actions_modal.dart | 39 +++++++++---------- .../lib/src/message_widget.dart | 7 ---- .../test/src/message_action_modal_test.dart | 2 - 3 files changed, 19 insertions(+), 29 deletions(-) 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..ac42dd0c 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -18,9 +18,6 @@ class MessageActionsModal extends StatefulWidget { required this.message, required this.messageWidget, required this.messageTheme, - this.showReactions = true, - this.showDeleteMessage = true, - this.showEditMessage = true, this.onReplyTap, this.onThreadReplyTap, this.showCopyMessage = true, @@ -28,7 +25,6 @@ class MessageActionsModal extends StatefulWidget { this.showResendMessage = true, this.showThreadReplyMessage = true, this.showFlagButton = true, - this.showPinButton = true, this.editMessageInputBuilder, this.reverse = false, this.customActions = const [], @@ -53,21 +49,12 @@ class MessageActionsModal extends StatefulWidget { /// [MessageThemeData] for message final MessageThemeData messageTheme; - /// Flag for showing reactions - final bool showReactions; - /// Callback when copy is tapped final OnMessageTap? onCopyTap; - /// Callback when delete is tapped - final bool showDeleteMessage; - /// Flag for showing copy action final bool showCopyMessage; - /// Flag for showing edit action - final bool showEditMessage; - /// Flag for showing resend action final bool showResendMessage; @@ -80,9 +67,6 @@ class MessageActionsModal extends StatefulWidget { /// Flag for showing flag action final bool showFlagButton; - /// Flag for showing pin action - final bool showPinButton; - /// Flag for reversing message final bool reverse; @@ -95,6 +79,7 @@ class MessageActionsModal extends StatefulWidget { class _MessageActionsModalState extends State { bool _showActions = true; + late List _userPermissions; @override Widget build(BuildContext context) => _showMessageOptionsModal(); @@ -138,7 +123,7 @@ class _MessageActionsModalState extends State { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - if (widget.showReactions && + if (_userPermissions.contains(PermissionType.sendReaction) && (widget.message.status == MessageSendingStatus.sent)) Align( alignment: Alignment( @@ -185,11 +170,16 @@ class _MessageActionsModalState extends State { _buildThreadReplyButton(context), if (widget.showResendMessage) _buildResendMessage(context), - if (widget.showEditMessage) _buildEditMessage(context), + if (_userPermissions + .contains(PermissionType.updateOwnMessage)) + _buildEditMessage(context), if (widget.showCopyMessage) _buildCopyButton(context), if (widget.showFlagButton) _buildFlagButton(context), - if (widget.showPinButton) _buildPinButton(context), - if (widget.showDeleteMessage) + if (_userPermissions + .contains(PermissionType.pinMessage)) + _buildPinButton(context), + if (_userPermissions + .contains(PermissionType.deleteOwnMessage)) _buildDeleteButton(context), ...widget.customActions .map((action) => _buildCustomAction( @@ -650,4 +640,13 @@ class _MessageActionsModalState extends State { ), ); } + + @override + void didChangeDependencies() { + final newStreamChannel = StreamChannel.of(context); + _userPermissions = + newStreamChannel.channel.state?.channelState.channel?.ownCapabilities ?? + []; + super.didChangeDependencies(); + } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index f6ae52c6..ce6c550d 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1104,7 +1104,6 @@ class _MessageWidgetState extends State Clipboard.setData(ClipboardData(text: message.text)), messageTheme: widget.messageTheme, reverse: widget.reverse, - showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, message: widget.message, editMessageInputBuilder: widget.editMessageInputBuilder, onReplyTap: widget.onReplyTap, @@ -1114,11 +1113,6 @@ class _MessageWidgetState extends State showCopyMessage: widget.showCopyMessage && !isFailedState && widget.message.text?.trim().isNotEmpty == true, - showEditMessage: widget.showEditMessage && - !isDeleteFailed && - !widget.message.attachments - .any((element) => element.type == 'giphy'), - showReactions: widget.showReactions, showReplyMessage: widget.showReplyMessage && !isFailedState && widget.onReplyTap != null, @@ -1126,7 +1120,6 @@ class _MessageWidgetState extends State !isFailedState && widget.onThreadTap != null, showFlagButton: widget.showFlagButton, - showPinButton: widget.showPinButton, customActions: widget.customActions, ), ), 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..702e162c 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 @@ -78,9 +78,7 @@ void main() { client: client, child: SizedBox( child: MessageActionsModal( - showEditMessage: false, showCopyMessage: false, - showDeleteMessage: false, showReplyMessage: false, showThreadReplyMessage: false, message: Message( From 3460164b75629fdd88b254c5ca0eba24db89910e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 9 Nov 2021 16:00:49 +0530 Subject: [PATCH 020/112] updated action modal --- .../lib/src/message_actions_modal.dart | 60 +++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) 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 ac42dd0c..ae620f01 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Constructs a modal with actions for a message class MessageActionsModal extends StatefulWidget { @@ -18,13 +19,17 @@ class MessageActionsModal extends StatefulWidget { required this.message, required this.messageWidget, required this.messageTheme, + this.showReactions, + this.showDeleteMessage, + this.showEditMessage, this.onReplyTap, this.onThreadReplyTap, this.showCopyMessage = true, this.showReplyMessage = true, this.showResendMessage = true, - this.showThreadReplyMessage = true, + this.showThreadReplyMessage, this.showFlagButton = true, + this.showPinButton, this.editMessageInputBuilder, this.reverse = false, this.customActions = const [], @@ -49,12 +54,21 @@ class MessageActionsModal extends StatefulWidget { /// [MessageThemeData] for message final MessageThemeData messageTheme; + /// Flag for showing reactions + final bool? showReactions; + /// Callback when copy is tapped final OnMessageTap? onCopyTap; + /// Callback when delete is tapped + final bool? showDeleteMessage; + /// Flag for showing copy action final bool showCopyMessage; + /// Flag for showing edit action + final bool? showEditMessage; + /// Flag for showing resend action final bool showResendMessage; @@ -62,11 +76,14 @@ class MessageActionsModal extends StatefulWidget { final bool showReplyMessage; /// Flag for showing thread reply action - final bool showThreadReplyMessage; + final bool? showThreadReplyMessage; /// Flag for showing flag action final bool showFlagButton; + /// Flag for showing pin action + final bool? showPinButton; + /// Flag for reversing message final bool reverse; @@ -80,6 +97,7 @@ class MessageActionsModal extends StatefulWidget { class _MessageActionsModalState extends State { bool _showActions = true; late List _userPermissions; + late bool _isMyMessage; @override Widget build(BuildContext context) => _showMessageOptionsModal(); @@ -114,6 +132,23 @@ class _MessageActionsModalState extends State { final shiftFactor = numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; + final hasEditPermission = + _userPermissions.contains( + PermissionType.updateAnyMessage, + ) || + _userPermissions.contains(PermissionType.updateOwnMessage); + + final hasDeletePermission = + _userPermissions.contains( + PermissionType.deleteAnyMessage, + ) || + _userPermissions.contains(PermissionType.deleteOwnMessage); + + final hasReactionPermission = + _userPermissions.contains(PermissionType.sendReaction); + + print(widget.showReactions); + final child = Center( child: SingleChildScrollView( child: Padding( @@ -123,7 +158,7 @@ class _MessageActionsModalState extends State { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - if (_userPermissions.contains(PermissionType.sendReaction) && + if ((widget.showReactions ?? hasReactionPermission) && (widget.message.status == MessageSendingStatus.sent)) Align( alignment: Alignment( @@ -163,23 +198,26 @@ class _MessageActionsModalState extends State { if (widget.showReplyMessage && widget.message.status == MessageSendingStatus.sent) _buildReplyButton(context), - if (widget.showThreadReplyMessage && + if ((widget.showThreadReplyMessage ?? + _userPermissions + .contains(PermissionType.sendReply)) && (widget.message.status == MessageSendingStatus.sent) && widget.message.parentId == null) _buildThreadReplyButton(context), if (widget.showResendMessage) _buildResendMessage(context), - if (_userPermissions - .contains(PermissionType.updateOwnMessage)) + if (widget.showEditMessage ?? + _isMyMessage && hasEditPermission) _buildEditMessage(context), if (widget.showCopyMessage) _buildCopyButton(context), if (widget.showFlagButton) _buildFlagButton(context), - if (_userPermissions - .contains(PermissionType.pinMessage)) + if (widget.showPinButton ?? + _userPermissions + .contains(PermissionType.pinMessage)) _buildPinButton(context), - if (_userPermissions - .contains(PermissionType.deleteOwnMessage)) + if (widget.showDeleteMessage ?? + _isMyMessage && hasDeletePermission) _buildDeleteButton(context), ...widget.customActions .map((action) => _buildCustomAction( @@ -647,6 +685,8 @@ class _MessageActionsModalState extends State { _userPermissions = newStreamChannel.channel.state?.channelState.channel?.ownCapabilities ?? []; + _isMyMessage = widget.message.user!.id == + newStreamChannel.channel.client.state.currentUser!.id; super.didChangeDependencies(); } } From 13b0fa0a4c835900356290bcf32cd85cc661f286 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 9 Nov 2021 16:16:11 +0530 Subject: [PATCH 021/112] added docs and reactions modal --- .../stream_chat/lib/src/permission_type.dart | 80 ++++++++++++++----- .../lib/src/message_reactions_modal.dart | 16 +++- 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/packages/stream_chat/lib/src/permission_type.dart b/packages/stream_chat/lib/src/permission_type.dart index 2e6eef64..b0e2e73f 100644 --- a/packages/stream_chat/lib/src/permission_type.dart +++ b/packages/stream_chat/lib/src/permission_type.dart @@ -1,37 +1,81 @@ +/// Describes capabilities of a user vis-a-vis a channel class PermissionType { - static const String sendMessage = "send-message"; + /// Capability required to send a message in the channel + /// Channel is not frozen (or user has UseFrozenChannel permission) + /// and user has CreateMessage permission. + static const String sendMessage = 'send-message'; - static const String sendReaction = "send-reaction"; + /// Capability required to send a message + /// Reactions are enabled for the channel, channel is not frozen + /// (or user has UseFrozenChannel permission) and user has + /// CreateReaction permission + static const String sendReaction = 'send-reaction'; - static const String sendLinks = "send-links"; + /// Capability required to send links in a channel + /// send-message + user has AddLinks permission + static const String sendLinks = 'send-links'; - static const String sendReply = "send-reply"; + /// Capability required to send thread reply + /// send-message + channel has replies enabled + static const String sendReply = 'send-reply'; - static const String freezeChannel = "freeze-channel"; + /// Capability to freeze a channel + /// User has UpdateChannelFrozen permission. + /// The name implies freezing, + /// but unfreezing is also allowed when this capability is present + static const String freezeChannel = 'freeze-channel'; - static const String setChannelCooldown = "set-channel-cooldown"; + /// User has UpdateChannelCooldown permission. + /// Allows to enable/disable slow mode in the channel + static const String setChannelCooldown = 'set-channel-cooldown'; - static const String leaveChannel = "leave-channel"; + /// User has RemoveOwnChannelMembership or UpdateChannelMembers permission + static const String leaveChannel = 'leave-channel'; - static const String pinMessage = "pin-message"; + /// Capability required to pin a message in a channel + /// Corresponds to PinMessage permission + static const String pinMessage = 'pin-message'; - static const String deleteAnyMessage = "delete-any-message"; + /// User has ability to delete any message in the channel + /// User has DeleteMessage permission + /// which applies to any message in the channel + static const String deleteAnyMessage = 'delete-any-message'; - static const String deleteOwnMessage = "delete-own-message"; + /// User has ability to delete their own message in the channel + /// User has DeleteMessage permission which applies only to owned messages + static const String deleteOwnMessage = 'delete-own-message'; - static const String updateAnyMessage = "update-any-message"; + /// User has ability to update/edit any message in the channel + /// User has UpdateMessage permission which + /// applies to any message in the channel + static const String updateAnyMessage = 'update-any-message'; - static const String updateOwnMessage = "update-own-message"; + /// User has ability to update/edit their own message in the channel + /// User has UpdateMessage permission which applies only to owned messages + static const String updateOwnMessage = 'update-own-message'; - static const String searchMessages = "search-messages"; + /// User can search for message in a channel + /// Search feature is enabled (it will also have + /// permission check in the future) + static const String searchMessages = 'search-messages'; - static const String sendTypingEvents = "send-typing-events"; + /// Capability required to send typing events in a channel + /// (Typing events are enabled) + static const String sendTypingEvents = 'send-typing-events'; - static const String uploadFile = "upload-file"; + /// Capability required to upload a file in a channel + /// Uploads are enabled and user has UploadAttachment + static const String uploadFile = 'upload-file'; - static const String deleteChannel = "delete-channel"; + /// Capability required to delete channel + /// User has DeleteChannel permission + static const String deleteChannel = 'delete-channel'; - static const String updateChannel = "update-channel"; + /// Capability required update/edit channel info + /// User has UpdateChannel permission + static const String updateChannel = 'update-channel'; - static const String updateChannelMembers = "update-channel-members"; + /// Capability required to update/edit channel members + /// Channel is not distinct and user has UpdateChannelMembers permission + static const String updateChannelMembers = 'update-channel-members'; } diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 6a73c439..4b85412c 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -18,7 +18,7 @@ class MessageReactionsModal extends StatelessWidget { required this.message, required this.messageWidget, required this.messageTheme, - this.showReactions = true, + this.showReactions, this.reverse = false, this.onUserAvatarTap, }) : super(key: key); @@ -36,7 +36,7 @@ class MessageReactionsModal extends StatelessWidget { final bool reverse; /// Flag to show reactions on message - final bool showReactions; + final bool? showReactions; /// Callback when user avatar is tapped final void Function(User)? onUserAvatarTap; @@ -45,6 +45,16 @@ class MessageReactionsModal extends StatelessWidget { Widget build(BuildContext context) { final size = MediaQuery.of(context).size; final user = StreamChat.of(context).currentUser; + final _userPermissions = StreamChannel.of(context) + .channel + .state + ?.channelState + .channel + ?.ownCapabilities ?? + []; + + final hasReactionPermission = + _userPermissions.contains(PermissionType.sendReaction); final roughMaxSize = size.width * 2 / 3; var messageTextLength = message.text!.length; @@ -76,7 +86,7 @@ class MessageReactionsModal extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (showReactions && + if ((showReactions ?? hasReactionPermission) && (message.status == MessageSendingStatus.sent)) Align( alignment: Alignment( From c9f848a6934cb3643f6d5e6b0eaf7843f2b62777 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 9 Nov 2021 16:20:51 +0530 Subject: [PATCH 022/112] added changelog --- packages/stream_chat/CHANGELOG.md | 6 ++++++ packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 4b409536..386f1937 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +✅ Added + +- `ChannelModel` now supplies individual user capabilities. + ## 3.2.0 🐞 Fixed diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 00f8412b..0b7e3c15 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🛑️ Breaking Changes from `3.2.0` + +- `pinPermissions` is no longer needed in `MessageListView`. + ## 3.2.0 - Updated Dart SDK constraints to `>=2.14.0 <3.0.0` From 8a1aa66a46f0aca55c15d56426a19f6dc5f356ea Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 9 Nov 2021 16:24:42 +0530 Subject: [PATCH 023/112] fmt --- .../stream_chat_flutter/lib/src/message_actions_modal.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 ae620f01..3bcb20cd 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -132,14 +132,12 @@ class _MessageActionsModalState extends State { final shiftFactor = numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; - final hasEditPermission = - _userPermissions.contains( + final hasEditPermission = _userPermissions.contains( PermissionType.updateAnyMessage, ) || _userPermissions.contains(PermissionType.updateOwnMessage); - final hasDeletePermission = - _userPermissions.contains( + final hasDeletePermission = _userPermissions.contains( PermissionType.deleteAnyMessage, ) || _userPermissions.contains(PermissionType.deleteOwnMessage); From 616881d141922a320db23dbabab9b3bc9e9161d5 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 9 Nov 2021 16:26:00 +0530 Subject: [PATCH 024/112] always remember to remove your print statements that you use instead of using the actual debugger --- packages/stream_chat_flutter/lib/src/message_actions_modal.dart | 2 -- 1 file changed, 2 deletions(-) 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 3bcb20cd..61bda279 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -145,8 +145,6 @@ class _MessageActionsModalState extends State { final hasReactionPermission = _userPermissions.contains(PermissionType.sendReaction); - print(widget.showReactions); - final child = Center( child: SingleChildScrollView( child: Padding( From 0b0ff753d49f8ff60af81eaa2ca9c1281892b1ce Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 1 Dec 2021 10:52:45 +0100 Subject: [PATCH 025/112] 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 fcb9e308c714ea3a6a4ca2f6570175d73c49dd0c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 17 Nov 2021 06:11:46 +0530 Subject: [PATCH 026/112] chore(ui): initial channel list view draft with controller. Signed-off-by: xsahil03x --- .../example/lib/tutorial_part_2.dart | 45 +- .../lib/src/channel_info.dart | 11 +- .../lib/src/group_avatar.dart | 5 +- .../lib/src/option_list_tile.dart | 17 +- .../lib/src/paged_value_notifier.dart | 92 +++ .../lib/src/paged_value_notifier.freezed.dart | 590 ++++++++++++++++++ .../lib/src/thread_header.dart | 13 +- .../lib/src/typing_indicator.dart | 41 +- .../stream_channel_list_controller.dart | 91 +++ .../stream_channel_list_loading_tile.dart | 91 +++ .../stream_channel_list_tile.dart | 379 +++++++++++ .../stream_channel_list_view.dart | 369 +++++++++++ .../lib/src/v4/stream_channel_avatar.dart | 201 ++++++ .../lib/src/v4/stream_channel_name.dart | 93 +++ .../lib/stream_chat_flutter.dart | 2 + 15 files changed, 1983 insertions(+), 57 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/paged_value_notifier.dart create mode 100644 packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart create mode 100644 packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 6abc260e..6e1edaba 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -62,29 +62,52 @@ class MyApp extends StatelessWidget { client: client, child: child, ), - home: const ChannelListPage(), + home: ChannelListPage( + client: client, + ), ); } } -class ChannelListPage extends StatelessWidget { - const ChannelListPage({ +class ChannelListPage extends StatefulWidget { + ChannelListPage({ Key? key, + required this.client, }) : super(key: key); + final StreamChatClient client; + + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _controller = StreamChannelListController( + client: widget.client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + ); + @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 62bfd1de..0c4f4ac7 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -94,11 +94,12 @@ class ChannelInfo extends StatelessWidget { return alternativeWidget ?? const Offstage(); } - return TypingIndicator( - parentId: parentId, - alignment: Alignment.center, - alternativeWidget: alternativeWidget, - style: textStyle, + return Align( + child: TypingIndicator( + parentId: parentId, + style: textStyle, + alternativeWidget: alternativeWidget, + ), ); } diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/group_avatar.dart index bf599c63..d7996c4d 100644 --- a/packages/stream_chat_flutter/lib/src/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/group_avatar.dart @@ -6,6 +6,7 @@ class GroupAvatar extends StatelessWidget { /// Constructor for creating a [GroupAvatar] const GroupAvatar({ Key? key, + this.channel, required this.members, this.constraints, this.onTap, @@ -15,6 +16,8 @@ class GroupAvatar extends StatelessWidget { this.selectionThickness = 4, }) : super(key: key); + final Channel? channel; + /// List of images to display final List members; @@ -38,7 +41,7 @@ class GroupAvatar extends StatelessWidget { @override Widget build(BuildContext context) { - final channel = StreamChannel.of(context).channel; + final channel = this.channel ?? StreamChannel.of(context).channel; assert(channel.state != null, 'Channel ${channel.id} is not initialized'); diff --git a/packages/stream_chat_flutter/lib/src/option_list_tile.dart b/packages/stream_chat_flutter/lib/src/option_list_tile.dart index 16bf9e97..b8daa0e3 100644 --- a/packages/stream_chat_flutter/lib/src/option_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/option_list_tile.dart @@ -6,7 +6,7 @@ class OptionListTile extends StatelessWidget { /// Constructor for creating [OptionListTile] const OptionListTile({ Key? key, - this.title, + required this.title, this.leading, this.trailing, this.onTap, @@ -17,7 +17,7 @@ class OptionListTile extends StatelessWidget { }) : super(key: key); /// Title for tile - final String? title; + final String title; /// Leading widget (start) final Widget? leading; @@ -46,8 +46,8 @@ class OptionListTile extends StatelessWidget { return Column( children: [ Container( - color: separatorColor ?? chatThemeData.colorTheme.disabled, height: 1, + color: separatorColor ?? chatThemeData.colorTheme.disabled, ), Material( color: tileColor ?? chatThemeData.colorTheme.barsBg, @@ -57,15 +57,14 @@ class OptionListTile extends StatelessWidget { onTap: onTap, child: Row( children: [ - if (leading != null) Center(child: leading), - if (leading == null) - const SizedBox( - width: 16, - ), + if (leading != null) + Center(child: leading) + else + const SizedBox(width: 16), Expanded( flex: 4, child: Text( - title!, + title, style: titleTextStyle ?? (titleColor == null ? chatThemeData.textTheme.bodyBold diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart new file mode 100644 index 00000000..d71db24d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -0,0 +1,92 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:stream_chat/stream_chat.dart' show StreamChatError; + +part 'paged_value_notifier.freezed.dart'; + +const defaultInitialPagedLimitMultiplier = 3; + +typedef PagedValueListenableBuilder + = ValueListenableBuilder>; + +abstract class PagedValueNotifier + extends ValueNotifier> { + /// Creates a [PagedValueNotifier] + PagedValueNotifier(this._initialValue) : super(_initialValue); + + /// Stores initialValue in case we need to call [refresh]. + final PagedValue _initialValue; + + /// Retry any failed load requests. + /// + /// Unlike [refresh], this does not resets the whole [value], + /// it only retries the last failed load request. + Future retry() { + var lastValue = value; + assert(lastValue.hasError, ''); + lastValue = lastValue as Success; + + final nextPageKey = lastValue.nextPageKey; + // resetting the error + value = lastValue.copyWith(error: null); + return loadMore(nextPageKey!); + } + + /// Refresh the data presented by this [PagedValueNotifier]. + /// + /// Note: This API is intended for UI-driven refresh signals, + /// such as swipe-to-refresh. + Future refresh() { + value = _initialValue; + return doInitialLoad(); + } + + /// Load initial data from the server. + Future doInitialLoad(); + + /// Load more data from the server using [nextPageKey]. + Future loadMore(Key nextPageKey); +} + +@freezed +abstract class PagedValue with _$PagedValue { + const PagedValue._(); + + /// Creates a new instance of [PagedValue] with the given [key] and [value]. + // @Assert( + // 'nextPageKey != null', + // 'Cannot set an error if all the pages are already fetched', + // ) + const factory PagedValue({ + /// List with all items loaded so far. + required List items, + + /// The key for the next page to be fetched. + Key? nextPageKey, + + /// The current error, if any. + StreamChatError? error, + }) = Success; + + bool get hasNextPage { + assert(this is Success, ''); + return (this as Success).nextPageKey != null; + } + + bool get hasError { + assert(this is Success, ''); + return (this as Success).error != null; + } + + int get itemCount { + assert(this is Success, ''); + final count = (this as Success).items.length; + if (hasNextPage || hasError) return count + 1; + return count; + } + + const factory PagedValue.loading() = Loading; + + const factory PagedValue.error(StreamChatError error) = Error; +} diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart new file mode 100644 index 00000000..515a9742 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart @@ -0,0 +1,590 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'paged_value_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +class _$PagedValueTearOff { + const _$PagedValueTearOff(); + + Success call( + {required List items, Key? nextPageKey, StreamChatError? error}) { + return Success( + items: items, + nextPageKey: nextPageKey, + error: error, + ); + } + + Loading loading() { + return Loading(); + } + + Error error(StreamChatError error) { + return Error( + error, + ); + } +} + +/// @nodoc +const $PagedValue = _$PagedValueTearOff(); + +/// @nodoc +mixin _$PagedValue { + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PagedValueCopyWith { + factory $PagedValueCopyWith(PagedValue value, + $Res Function(PagedValue) then) = + _$PagedValueCopyWithImpl; +} + +/// @nodoc +class _$PagedValueCopyWithImpl + implements $PagedValueCopyWith { + _$PagedValueCopyWithImpl(this._value, this._then); + + final PagedValue _value; + // ignore: unused_field + final $Res Function(PagedValue) _then; +} + +/// @nodoc +abstract class $SuccessCopyWith { + factory $SuccessCopyWith( + Success value, $Res Function(Success) then) = + _$SuccessCopyWithImpl; + $Res call({List items, Key? nextPageKey, StreamChatError? error}); +} + +/// @nodoc +class _$SuccessCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $SuccessCopyWith { + _$SuccessCopyWithImpl( + Success _value, $Res Function(Success) _then) + : super(_value, (v) => _then(v as Success)); + + @override + Success get _value => super._value as Success; + + @override + $Res call({ + Object? items = freezed, + Object? nextPageKey = freezed, + Object? error = freezed, + }) { + return _then(Success( + items: items == freezed + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + nextPageKey: nextPageKey == freezed + ? _value.nextPageKey + : nextPageKey // ignore: cast_nullable_to_non_nullable + as Key?, + error: error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StreamChatError?, + )); + } +} + +/// @nodoc + +class _$Success extends Success + with DiagnosticableTreeMixin { + const _$Success({required this.items, this.nextPageKey, this.error}) + : super._(); + + @override + + /// List with all items loaded so far. + final List items; + @override + + /// The key for the next page to be fetched. + final Key? nextPageKey; + @override + + /// The current error, if any. + final StreamChatError? error; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>(items: $items, nextPageKey: $nextPageKey, error: $error)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>')) + ..add(DiagnosticsProperty('items', items)) + ..add(DiagnosticsProperty('nextPageKey', nextPageKey)) + ..add(DiagnosticsProperty('error', error)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other is Success && + (identical(other.items, items) || + const DeepCollectionEquality().equals(other.items, items)) && + (identical(other.nextPageKey, nextPageKey) || + const DeepCollectionEquality() + .equals(other.nextPageKey, nextPageKey)) && + (identical(other.error, error) || + const DeepCollectionEquality().equals(other.error, error))); + } + + @override + int get hashCode => + runtimeType.hashCode ^ + const DeepCollectionEquality().hash(items) ^ + const DeepCollectionEquality().hash(nextPageKey) ^ + const DeepCollectionEquality().hash(error); + + @JsonKey(ignore: true) + @override + $SuccessCopyWith> get copyWith => + _$SuccessCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return $default(items, nextPageKey, this.error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return $default?.call(items, nextPageKey, this.error); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(items, nextPageKey, this.error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return $default(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return $default?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(this); + } + return orElse(); + } +} + +abstract class Success extends PagedValue { + const factory Success( + {required List items, + Key? nextPageKey, + StreamChatError? error}) = _$Success; + const Success._() : super._(); + + /// List with all items loaded so far. + List get items => throw _privateConstructorUsedError; + + /// The key for the next page to be fetched. + Key? get nextPageKey => throw _privateConstructorUsedError; + + /// The current error, if any. + StreamChatError? get error => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $SuccessCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadingCopyWith { + factory $LoadingCopyWith( + Loading value, $Res Function(Loading) then) = + _$LoadingCopyWithImpl; +} + +/// @nodoc +class _$LoadingCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $LoadingCopyWith { + _$LoadingCopyWithImpl( + Loading _value, $Res Function(Loading) _then) + : super(_value, (v) => _then(v as Loading)); + + @override + Loading get _value => super._value as Loading; +} + +/// @nodoc + +class _$Loading extends Loading + with DiagnosticableTreeMixin { + const _$Loading() : super._(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>.loading()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.loading')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || (other is Loading); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class Loading extends PagedValue { + const factory Loading() = _$Loading; + const Loading._() : super._(); +} + +/// @nodoc +abstract class $ErrorCopyWith { + factory $ErrorCopyWith( + Error value, $Res Function(Error) then) = + _$ErrorCopyWithImpl; + $Res call({StreamChatError error}); +} + +/// @nodoc +class _$ErrorCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $ErrorCopyWith { + _$ErrorCopyWithImpl( + Error _value, $Res Function(Error) _then) + : super(_value, (v) => _then(v as Error)); + + @override + Error get _value => super._value as Error; + + @override + $Res call({ + Object? error = freezed, + }) { + return _then(Error( + error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StreamChatError, + )); + } +} + +/// @nodoc + +class _$Error extends Error + with DiagnosticableTreeMixin { + const _$Error(this.error) : super._(); + + @override + final StreamChatError error; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>.error(error: $error)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.error')) + ..add(DiagnosticsProperty('error', error)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other is Error && + (identical(other.error, error) || + const DeepCollectionEquality().equals(other.error, error))); + } + + @override + int get hashCode => + runtimeType.hashCode ^ const DeepCollectionEquality().hash(error); + + @JsonKey(ignore: true) + @override + $ErrorCopyWith> get copyWith => + _$ErrorCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return error(this.error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return error?.call(this.error); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this.error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class Error extends PagedValue { + const factory Error(StreamChatError error) = _$Error; + const Error._() : super._(); + + StreamChatError get error => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $ErrorCopyWith> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index dfe13b41..5f7fb25e 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -163,12 +163,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { ), const SizedBox(height: 2), if (showTypingIndicator) - TypingIndicator( - alignment: Alignment.center, - channel: StreamChannel.of(context).channel, - style: channelHeaderTheme.subtitleStyle, - parentId: parent.id, - alternativeWidget: defaultSubtitle, + Align( + child: TypingIndicator( + channel: StreamChannel.of(context).channel, + style: channelHeaderTheme.subtitleStyle, + parentId: parent.id, + alternativeWidget: defaultSubtitle, + ), ) else defaultSubtitle, diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index d961a8ff..389d1020 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -11,7 +11,6 @@ class TypingIndicator extends StatelessWidget { this.channel, this.alternativeWidget, this.style, - this.alignment = Alignment.centerLeft, this.padding = const EdgeInsets.all(0), this.parentId, }) : super(key: key); @@ -28,9 +27,6 @@ class TypingIndicator extends StatelessWidget { /// The padding of this widget final EdgeInsets padding; - /// Alignment of the typing indicator - final Alignment alignment; - /// Id of the parent message in case of a thread final String? parentId; @@ -46,30 +42,25 @@ class TypingIndicator extends StatelessWidget { stream: channelState.typingEventsStream.map((typings) => typings.entries .where((element) => element.value.parentId == parentId) .map((e) => e.key)), - builder: (context, data) => AnimatedSwitcher( + builder: (context, users) => AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: data.isNotEmpty + child: users.isNotEmpty ? Padding( - key: const Key('main'), padding: padding, - child: Align( - key: const Key('typings'), - alignment: alignment, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Lottie.asset( - 'animations/typing_dots.json', - package: 'stream_chat_flutter', - height: 4, - ), - Text( - context.translations.userTypingText(data), - maxLines: 1, - style: style, - ), - ], - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Lottie.asset( + 'animations/typing_dots.json', + package: 'stream_chat_flutter', + height: 4, + ), + Text( + context.translations.userTypingText(users), + maxLines: 1, + style: style, + ), + ], ), ) : altWidget, diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart new file mode 100644 index 00000000..4bfc7130 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -0,0 +1,91 @@ +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; + +class StreamChannelListController extends PagedValueNotifier { + /// Creates a [StreamChannelListController]. + StreamChannelListController({ + required this.client, + this.filter, + this.sort, + this.limit = 2, + this.messageLimit, + this.memberLimit, + }) : super(const PagedValue.loading()); + + /// Creates a [StreamChannelListController] from the passed [value]. + StreamChannelListController.fromValue( + PagedValue value, { + required this.client, + this.filter, + this.sort, + this.limit = 2, + this.messageLimit, + this.memberLimit, + }) : super(value); + + /// The client to use for the channel list. + final StreamChatClient client; + + /// The filter to apply to the channel list. + final Filter? filter; + + /// The sort to apply to the channel list. + final List>? sort; + + /// The limit to apply to the channel list. + final int limit; + + /// The limit to apply to the message list. + final int? messageLimit; + + /// The limit to apply to the member list. + final int? memberLimit; + + @override + Future doInitialLoad() async { + final limit = this.limit * defaultInitialPagedLimitMultiplier; + try { + await for (final channels in client.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + paginationParams: PaginationParams(limit: limit), + )) { + final nextKey = channels.length < limit ? null : channels.length; + value = PagedValue( + items: channels, + nextPageKey: nextKey, + ); + } + } catch (error) { + value = PagedValue.error(StreamChatError('error')); + } + } + + @override + Future loadMore(int nextPageKey) async { + assert(value is Success, ''); + final previousValue = value as Success; + + try { + await for (final channels in client.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + paginationParams: PaginationParams(limit: limit, offset: nextPageKey), + )) { + final previousItems = previousValue.items; + final newItems = previousItems + channels; + final nextKey = channels.length < limit ? null : newItems.length; + value = PagedValue( + items: newItems, + nextPageKey: nextKey, + ); + } + } catch (error) { + value = previousValue.copyWith(error: StreamChatError('error')); + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart new file mode 100644 index 00000000..9e0231d5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +class StreamChannelListLoadingTile extends StatelessWidget { + const StreamChannelListLoadingTile({ + Key? key, + this.visualDensity = VisualDensity.standard, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + }) : super(key: key); + + /// Defines how compact the list tile's layout will be. + /// + /// {@macro flutter.material.themedata.visualDensity} + /// + /// See also: + /// + /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all + /// widgets within a [Theme]. + final VisualDensity visualDensity; + + /// The tile's internal padding. + /// + /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], + /// and [trailing] widgets. + /// + /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. + final EdgeInsetsGeometry contentPadding; + + @override + Widget build(BuildContext context) { + final colorTheme = StreamChatTheme.of(context).colorTheme; + + final leading = Container( + height: 49, + width: 49, + decoration: BoxDecoration( + color: colorTheme.barsBg, + shape: BoxShape.circle, + ), + ); + + final title = Container( + height: 16, + width: 66, + decoration: BoxDecoration( + color: colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + ); + + final subtitle = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Container( + height: 16, + decoration: BoxDecoration( + color: colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + const SizedBox(width: 8), + Container( + height: 16, + width: 50, + decoration: BoxDecoration( + color: colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ); + + return Shimmer.fromColors( + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, + child: ListTile( + leading: leading, + title: title, + subtitle: subtitle, + visualDensity: visualDensity, + contentPadding: contentPadding, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart new file mode 100644 index 00000000..f4fb4fde --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -0,0 +1,379 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat/stream_chat.dart' show Channel; +import 'package:stream_chat_flutter/src/sending_indicator.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/channel_preview_theme.dart'; +import 'package:stream_chat_flutter/src/typing_indicator.dart'; +import 'package:stream_chat_flutter/src/unread_indicator.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_avatar.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; + +class StreamChannelListTile extends StatelessWidget { + StreamChannelListTile({ + Key? key, + required this.channel, + this.leading, + this.title, + this.subtitle, + this.onTap, + this.onLongPress, + this.visualDensity = VisualDensity.compact, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// A widget to display before the title. + /// + /// Typically an [Icon] or a [CircleAvatar] widget. + final Widget? leading; + + /// The primary content of the list tile. + /// + /// Typically a [Text] widget. + /// + /// This should not wrap. To enforce the single line limit, use + /// [Text.maxLines]. + final Widget? title; + + /// Additional content displayed below the title. + /// + /// Typically a [Text] widget. + /// + /// If [isThreeLine] is false, this should not wrap. + /// + /// If [isThreeLine] is true, this should be configured to take a maximum of + /// two lines. For example, you can use [Text.maxLines] to enforce the number + /// of lines. + /// + /// The subtitle's default [TextStyle] depends on [TextTheme.bodyText2] except + /// [TextStyle.color]. The [TextStyle.color] depends on the value of [enabled] + /// and [selected]. + /// + /// When [enabled] is false, the text color is set to [ThemeData.disabledColor]. + /// + /// When [selected] is false, the text color is set to [ListTileTheme.textColor] + /// if it's not null and to [TextTheme.caption]'s color if [ListTileTheme.textColor] + /// is null. + final Widget? subtitle; + + /// Called when the user taps this list tile. + /// + /// Inoperative if [enabled] is false. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this list tile. + /// + /// Inoperative if [enabled] is false. + final GestureLongPressCallback? onLongPress; + + /// Defines how compact the list tile's layout will be. + /// + /// {@macro flutter.material.themedata.visualDensity} + /// + /// See also: + /// + /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all + /// widgets within a [Theme]. + final VisualDensity visualDensity; + + /// The tile's internal padding. + /// + /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], + /// and [trailing] widgets. + /// + /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. + final EdgeInsetsGeometry contentPadding; + + @override + Widget build(BuildContext context) { + final channelState = channel.state!; + final currentUser = channel.client.state.currentUser!; + + final channelPreviewTheme = ChannelPreviewTheme.of(context); + + final leading = this.leading ?? + StreamChannelAvatar( + channel: channel, + ); + + final title = this.title ?? + StreamChannelName( + channel: channel, + textStyle: channelPreviewTheme.titleStyle, + ); + + final subtitle = this.subtitle ?? + ChannelListTileSubtitle( + channel: channel, + textStyle: channelPreviewTheme.subtitleStyle, + ); + + return BetterStreamBuilder( + stream: channel.isMutedStream, + initialData: channel.isMuted, + builder: (context, isMuted) => AnimatedOpacity( + opacity: isMuted ? 0.5 : 1, + duration: const Duration(milliseconds: 300), + child: ListTile( + onTap: onTap, + onLongPress: onLongPress, + visualDensity: visualDensity, + contentPadding: contentPadding, + leading: leading, + title: Row( + children: [ + Expanded(child: title), + BetterStreamBuilder>( + stream: channelState.membersStream, + initialData: channelState.members, + comparator: const ListEquality().equals, + builder: (context, members) { + if (members.isEmpty || + !members.any((it) => it.user!.id == currentUser.id)) { + return const Offstage(); + } + return UnreadIndicator(cid: channel.cid); + }, + ), + ], + ), + subtitle: Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: subtitle, + ), + ), + BetterStreamBuilder>( + stream: channelState.messagesStream, + initialData: channelState.messages, + comparator: const ListEquality().equals, + builder: (context, messages) { + final lastMessage = messages.lastWhereOrNull( + (m) => !m.shadowed && !m.isDeleted, + ); + + if (lastMessage == null || + (lastMessage.user?.id != currentUser.id)) { + return const Offstage(); + } + + return Padding( + padding: const EdgeInsets.only(right: 4), + child: SendingIndicator( + message: lastMessage, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: channelState.read + .where((it) => it.user.id != currentUser.id) + .where( + (it) => it.lastRead.isAfter(lastMessage.createdAt), + ) + .isNotEmpty, + ), + ); + }, + ), + ChannelLastMessageDate( + channel: channel, + textStyle: channelPreviewTheme.lastMessageAtStyle, + ), + // trailing ?? _buildDate(context), + ], + ), + ), + ), + ); + } +} + +class ChannelLastMessageDate extends StatelessWidget { + ChannelLastMessageDate({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + stream: channel.lastMessageAtStream, + initialData: channel.lastMessageAt, + builder: (context, data) { + final lastMessageAt = data.toLocal(); + + String stringDate; + final now = DateTime.now(); + + final startOfDay = DateTime(now.year, now.month, now.day); + + if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay.millisecondsSinceEpoch) { + stringDate = Jiffy(lastMessageAt.toLocal()).jm; + } else if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch) { + stringDate = context.translations.yesterdayLabel; + } else if (startOfDay.difference(lastMessageAt).inDays < 7) { + stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; + } else { + stringDate = Jiffy(lastMessageAt.toLocal()).yMd; + } + + return Text( + stringDate, + style: textStyle, + ); + }, + ); +} + +class ChannelListTileSubtitle extends StatelessWidget { + ChannelListTileSubtitle({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + if (channel.isMuted) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + StreamSvgIcon.mute(size: 16), + Text( + ' ${context.translations.channelIsMutedText}', + style: textStyle, + ), + ], + ); + } + return TypingIndicator( + channel: channel, + style: textStyle, + alternativeWidget: ChannelLastMessageText( + channel: channel, + textStyle: textStyle, + ), + ); + } +} + +class ChannelLastMessageText extends StatelessWidget { + ChannelLastMessageText({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) => BetterStreamBuilder>( + stream: channel.state!.messagesStream, + initialData: channel.state!.messages, + builder: (context, messages) { + final lastMessage = messages.lastWhereOrNull( + (m) => !m.shadowed && !m.isDeleted, + ); + + if (lastMessage == null) return const Offstage(); + + final lastMessageText = lastMessage.text; + final lastMessageAttachments = lastMessage.attachments; + final lastMessageMentionedUsers = lastMessage.mentionedUsers; + + final messageTextParts = [ + ...lastMessageAttachments.map((it) { + if (it.type == 'image') { + return '📷'; + } else if (it.type == 'video') { + return '🎬'; + } else if (it.type == 'giphy') { + return '[GIF]'; + } + return it == lastMessage.attachments.last + ? (it.title ?? 'File') + : '${it.title ?? 'File'} , '; + }), + if (lastMessageText != null) lastMessageText, + ]; + + final fontStyle = (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal; + + final regularTextStyle = textStyle?.copyWith(fontStyle: fontStyle); + + final mentionsTextStyle = textStyle?.copyWith( + fontStyle: fontStyle, + fontWeight: FontWeight.bold, + ); + + final spans = [ + for (final part in messageTextParts) + if (lastMessageMentionedUsers.isNotEmpty && + lastMessageMentionedUsers.any((it) => '@${it.name}' == part)) + TextSpan( + text: '$part ', + style: mentionsTextStyle, + ) + else if (lastMessageAttachments.isNotEmpty && + lastMessageAttachments + .where((it) => it.title != null) + .any((it) => it.title == part)) + TextSpan( + text: '$part ', + style: regularTextStyle, + ) + else + TextSpan( + text: part == messageTextParts.last ? part : '$part ', + style: regularTextStyle, + ), + ]; + + return Text.rich( + TextSpan(children: spans), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart new file mode 100644 index 00000000..ea9f5c1e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -0,0 +1,369 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/paged_value_notifier.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/v4/channel_list_view/stream_channel_list_controller.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Signature for a function that creates a widget for a given index, e.g., in a +/// list. +/// +/// Used by [GridView.builder] and other APIs that use lazily-generated widgets. +/// +/// See also: +/// +/// * [WidgetBuilder], which is similar but only takes a [BuildContext]. +/// * [TransitionBuilder], which is similar but also takes a child. +/// * [NullableIndexedWidgetBuilder], which is similar but may return null. +typedef StreamChannelListViewItemBuilder = Widget Function( + BuildContext context, + Channel channel, +); + +typedef StreamChannelTapCallback = void Function(Channel); + +Widget _defaultSeparatorBuilder(context, index) => + const _ChannelListSeparator(); + +class StreamChannelListView extends StatefulWidget { + const StreamChannelListView({ + Key? key, + required this.controller, + this.itemBuilder, + this.separatorBuilder = _defaultSeparatorBuilder, + this.onChannelTap, + this.onChannelLongPress, + this.padding, + this.physics, + this.reverse = false, + this.scrollController, + this.primary, + this.scrollBehavior, + this.shrinkWrap = false, + this.cacheExtent, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + }) : super(key: key); + + final StreamChannelListController controller; + + final StreamChannelListViewItemBuilder? itemBuilder; + + final IndexedWidgetBuilder separatorBuilder; + + /// Called when the user taps this list tile. + /// + /// Inoperative if [enabled] is false. + final StreamChannelTapCallback? onChannelTap; + + /// Called when the user long-presses on this list tile. + /// + /// Inoperative if [enabled] is false. + final StreamChannelTapCallback? onChannelLongPress; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by an otherwise focused part of the application, + /// the ScrollAction will be evaluated using this scroll view, for example, + /// when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollController] is null. + final bool? primary; + + /// {@macro flutter.widgets.shadow.scrollBehavior} + /// + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + @override + _StreamChannelListViewState createState() => _StreamChannelListViewState(); +} + +class _StreamChannelListViewState extends State { + StreamChannelListController get _controller => widget.controller; + + // Avoids duplicate requests on rebuilds. + bool _hasRequestedNextPage = false; + + @override + void initState() { + super.initState(); + _controller.doInitialLoad(); + } + + @override + void didUpdateWidget(covariant StreamChannelListView oldWidget) { + super.didUpdateWidget(oldWidget); + if (_controller != oldWidget.controller) { + // reset duplicate requests flag + _hasRequestedNextPage = false; + _controller.doInitialLoad(); + } + } + + @override + Widget build(BuildContext context) => + PagedValueListenableBuilder( + valueListenable: widget.controller, + builder: (context, value, _) => value.when( + (channels, nextPageKey, error) { + if (channels.isEmpty) { + return const Center(child: Text('No channels')); + } + + return ListView.separated( + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + controller: widget.scrollController, + primary: widget.primary, + shrinkWrap: widget.shrinkWrap, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + dragStartBehavior: widget.dragStartBehavior, + cacheExtent: widget.cacheExtent, + itemCount: value.itemCount, + separatorBuilder: widget.separatorBuilder, + itemBuilder: (context, index) { + if (!_hasRequestedNextPage) { + final newPageRequestTriggerIndex = value.itemCount - 3; + final isBuildingTriggerIndexItem = + index == newPageRequestTriggerIndex; + if (value.hasNextPage && isBuildingTriggerIndexItem) { + // Schedules the request for the end of this frame. + WidgetsBinding.instance?.addPostFrameCallback((_) async { + if (!value.hasError) { + await _controller.loadMore(nextPageKey!); + } + _hasRequestedNextPage = false; + }); + _hasRequestedNextPage = true; + } + } + + if (index == channels.length) { + if (value.hasError) { + return _ChannelListLoadMoreError( + onTap: _controller.retry, + ); + } + return const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: _ChannelListLoadMoreIndicator(), + ), + ); + } + + final channel = channels[index]; + final itemBuilder = widget.itemBuilder; + if (itemBuilder != null) return itemBuilder(context, channel); + + final onTap = widget.onChannelTap; + final onLongPress = widget.onChannelLongPress; + + return StreamChannelListTile( + channel: channel, + onTap: onTap == null ? null : () => onTap(channel), + onLongPress: + onLongPress == null ? null : () => onLongPress(channel), + ); + }, + ); + }, + loading: () => ListView.separated( + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + itemCount: 25, + separatorBuilder: widget.separatorBuilder, + itemBuilder: (_, __) => const StreamChannelListLoadingTile(), + ), + error: (error) => Center(child: Text('Error: $error')), + ), + ); +} + +class _ChannelListLoadMoreIndicator extends StatelessWidget { + const _ChannelListLoadMoreIndicator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator.adaptive(), + ); +} + +class _ChannelListLoadMoreError extends StatelessWidget { + const _ChannelListLoadMoreError({ + Key? key, + required this.onTap, + }) : super(key: key); + + final GestureTapCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + return InkWell( + onTap: onTap, + child: Container( + color: theme.colorTheme.textLowEmphasis.withOpacity(0.9), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.translations.loadingChannelsError, + style: theme.textTheme.body.copyWith( + color: Colors.white, + ), + ), + StreamSvgIcon.retry(color: Colors.white), + ], + ), + ), + ), + ); + } +} + +class _ChannelListSeparator extends StatelessWidget { + const _ChannelListSeparator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final effect = StreamChatTheme.of(context).colorTheme.borderBottom; + return Container( + height: 1, + color: effect.color!.withOpacity(effect.alpha ?? 1.0), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart new file mode 100644 index 00000000..e1a8cdeb --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart @@ -0,0 +1,201 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/group_avatar.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image_paint.png) +/// +/// It shows the current [Channel] image. +/// +/// ```dart +/// class MyApp extends StatelessWidget { +/// final StreamChatClient client; +/// final Channel channel; +/// +/// MyApp(this.client, this.channel); +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// debugShowCheckedModeBanner: false, +/// home: StreamChat( +/// client: client, +/// child: StreamChannel( +/// channel: channel, +/// child: Center( +/// child: ChannelImage( +/// channel: channel, +/// ), +/// ), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +/// +/// By default the widget radius size is 40x40 pixels. +/// Set the property [constraints] to set a custom dimension. +/// +/// The widget renders the ui based on the first ancestor of type +/// [StreamChatTheme]. +/// Modify it to change the widget appearance. +class StreamChannelAvatar extends StatelessWidget { + /// Instantiate a new ChannelImage + StreamChannelAvatar({ + Key? key, + required this.channel, + this.constraints, + this.onTap, + this.borderRadius, + this.selected = false, + this.selectionColor, + this.selectionThickness = 4, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// [BorderRadius] to display the widget + final BorderRadius? borderRadius; + + /// The channel to show the image of + final Channel channel; + + /// The diameter of the image + final BoxConstraints? constraints; + + /// The function called when the image is tapped + final VoidCallback? onTap; + + /// If image is selected + final bool selected; + + /// Selection color for image + final Color? selectionColor; + + /// Thickness of selection image + final double selectionThickness; + + @override + Widget build(BuildContext context) { + final client = channel.client.state; + + final chatThemeData = StreamChatTheme.of(context); + final colorTheme = chatThemeData.colorTheme; + final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; + + return BetterStreamBuilder( + stream: channel.imageStream, + initialData: channel.image, + builder: (context, channelImage) { + Widget child = ClipRRect( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + child: Container( + constraints: constraints ?? previewTheme?.constraints, + decoration: BoxDecoration(color: colorTheme.accentPrimary), + child: InkWell( + onTap: onTap, + child: CachedNetworkImage( + imageUrl: channelImage, + errorWidget: (_, __, ___) => Center( + child: Text( + channel.name?[0] ?? '', + style: TextStyle( + color: colorTheme.barsBg, + fontWeight: FontWeight.bold, + ), + ), + ), + fit: BoxFit.cover, + ), + ), + ), + ); + + if (selected) { + child = ClipRRect( + key: const Key('selectedImage'), + borderRadius: BorderRadius.circular(selectionThickness) + + (borderRadius ?? + previewTheme?.borderRadius ?? + BorderRadius.zero), + child: Container( + constraints: constraints ?? previewTheme?.constraints, + color: selectionColor ?? colorTheme.accentPrimary, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: child, + ), + ), + ); + } + return child; + }, + noDataBuilder: (context) { + final currentUser = client.currentUser!; + final otherMembers = channel.state!.members + .where((it) => it.userId != currentUser.id) + .toList(growable: false); + + // our own space, no other members + if (otherMembers.isEmpty) { + return BetterStreamBuilder( + stream: client.currentUserStream.map((it) => it!), + initialData: currentUser, + builder: (context, user) => UserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: user, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // 1-1 Conversation + if (otherMembers.length == 1) { + final member = otherMembers.first; + return BetterStreamBuilder( + stream: channel.state!.membersStream.map( + (members) => members.firstWhere( + (it) => it.userId == member.userId, + orElse: () => member, + ), + ), + initialData: member, + builder: (context, member) => UserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: member.user!, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // Group conversation + return GroupAvatar( + channel: channel, + members: otherMembers, + borderRadius: borderRadius ?? previewTheme?.borderRadius, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart new file mode 100644 index 00000000..2bcd56e8 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// It shows the current [Channel] name using a [Text] widget. +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +class StreamChannelName extends StatelessWidget { + /// Instantiate a new ChannelName + StreamChannelName({ + Key? key, + required this.channel, + this.textStyle, + this.textOverflow = TextOverflow.ellipsis, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + /// How visual overflow should be handled. + final TextOverflow textOverflow; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + stream: channel.nameStream, + initialData: channel.name, + builder: (context, channelName) => Text( + channelName, + style: textStyle, + overflow: textOverflow, + ), + noDataBuilder: (context) => _generateName( + channel.client.state.currentUser!, + channel.state!.members, + ), + ); + + Widget _generateName( + User currentUser, + List members, + ) => + LayoutBuilder( + builder: (context, constraints) { + var channelName = context.translations.noTitleText; + final otherMembers = members.where( + (member) => member.userId != currentUser.id, + ); + + if (otherMembers.isNotEmpty) { + if (otherMembers.length == 1) { + final user = otherMembers.first.user; + if (user != null) { + channelName = user.name; + } + } else { + final maxWidth = constraints.maxWidth; + final maxChars = maxWidth / (textStyle?.fontSize ?? 1); + var currentChars = 0; + final currentMembers = []; + otherMembers.forEach((element) { + final newLength = + currentChars + (element.user?.name.length ?? 0); + if (newLength < maxChars) { + currentChars = newLength; + currentMembers.add(element); + } + }); + + final exceedingMembers = + otherMembers.length - currentMembers.length; + channelName = + '${currentMembers.map((e) => e.user?.name).join(', ')} ' + '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } + } + + return Text( + channelName, + style: textStyle, + overflow: textOverflow, + ); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index cd0d93a0..ce82789a 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -48,3 +48,5 @@ export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; export 'src/visible_footnote.dart'; +export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; From 2cab266c90ccd173edd3711994de2b957d25c99d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Nov 2021 17:33:19 +0530 Subject: [PATCH 027/112] feat(ui): add channel list event handler in channel list controller. Signed-off-by: xsahil03x --- .../lib/src/paged_value_notifier.dart | 70 +++- .../stream_channel_list_controller.dart | 213 +++++++++++- .../stream_channel_list_event_handler.dart | 311 ++++++++++++++++++ .../stream_channel_list_view.dart | 91 +++-- 4 files changed, 628 insertions(+), 57 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart index d71db24d..d2f5b373 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -10,6 +10,14 @@ const defaultInitialPagedLimitMultiplier = 3; typedef PagedValueListenableBuilder = ValueListenableBuilder>; +/// A [PagedValueNotifier] that uses a [PagedListenable] to load data. +/// +/// This class is useful when you need to load data from a server +/// using a [PagedListenable] and want to keep the UI-driven refresh +/// signals in the [PagedListenable]. +/// +/// [PagedValueNotifier] is a [ValueNotifier] that emits a [PagedValue] +/// whenever the data is loaded or an error occurs. abstract class PagedValueNotifier extends ValueNotifier> { /// Creates a [PagedValueNotifier] @@ -18,14 +26,33 @@ abstract class PagedValueNotifier /// Stores initialValue in case we need to call [refresh]. final PagedValue _initialValue; + /// Returns the currently loaded items + List get currentItems => value.asSuccess.items; + + /// Appends [newItems] to the previously loaded ones and replaces + /// the next page's key. + void appendPage({ + required List newItems, + required Key nextPageKey, + }) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems, nextPageKey: nextPageKey); + } + + /// Appends [newItems] to the previously loaded ones and sets the next page + /// key to `null`. + void appendLastPage(List newItems) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems); + } + /// Retry any failed load requests. /// /// Unlike [refresh], this does not resets the whole [value], /// it only retries the last failed load request. Future retry() { - var lastValue = value; + final lastValue = value.asSuccess; assert(lastValue.hasError, ''); - lastValue = lastValue as Success; final nextPageKey = lastValue.nextPageKey; // resetting the error @@ -53,7 +80,7 @@ abstract class PagedValueNotifier abstract class PagedValue with _$PagedValue { const PagedValue._(); - /// Creates a new instance of [PagedValue] with the given [key] and [value]. + /// Represents the success state of the [PagedValue] // @Assert( // 'nextPageKey != null', // 'Cannot set an error if all the pages are already fetched', @@ -69,24 +96,35 @@ abstract class PagedValue with _$PagedValue { StreamChatError? error, }) = Success; - bool get hasNextPage { - assert(this is Success, ''); - return (this as Success).nextPageKey != null; + /// Represents the loading state of the [PagedValue]. + const factory PagedValue.loading() = Loading; + + /// Represents the error state of the [PagedValue]. + const factory PagedValue.error(StreamChatError error) = Error; + + /// Returns `true` if the [PagedValue] is [Success]. + bool get isSuccess => this is Success; + + /// Returns the [PagedValue] as [Success]. + Success get asSuccess { + assert( + isSuccess, + 'Cannot get asSuccess if the PagedValue is not in the Success state', + ); + return this as Success; } - bool get hasError { - assert(this is Success, ''); - return (this as Success).error != null; - } + /// Returns `true` if the [PagedValue] is [Success] + /// and has more items to load. + bool get hasNextPage => asSuccess.nextPageKey != null; + /// Returns `true` if the [PagedValue] is [Success] and has an error. + bool get hasError => asSuccess.error != null; + + /// int get itemCount { - assert(this is Success, ''); - final count = (this as Success).items.length; + final count = asSuccess.items.length; if (hasNextPage || hasError) return count + 1; return count; } - - const factory PagedValue.loading() = Loading; - - const factory PagedValue.error(StreamChatError error) = Error; } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 4bfc7130..d4715619 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -1,15 +1,40 @@ +import 'dart:async'; + import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' + as event_handler; +const defaultChannelPagedLimit = 10; + +typedef ChannelListEventHandler = void Function( + Event event, + StreamChannelListController controller, +); + +/// A controller for the channel list view. class StreamChannelListController extends PagedValueNotifier { /// Creates a [StreamChannelListController]. StreamChannelListController({ required this.client, this.filter, this.sort, - this.limit = 2, + this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, + this.onChannelDeleted = event_handler.onChannelDeleted, + this.onChannelHidden = event_handler.onChannelHidden, + this.onChannelTruncated = event_handler.onChannelTruncated, + this.onChannelUpdated = event_handler.onChannelUpdated, + this.onChannelVisible = event_handler.onChannelVisible, + this.onConnectionRecovered = event_handler.onConnectionRecovered, + this.onMessageNew = event_handler.onMessageNew, + this.onNotificationAddedToChannel = + event_handler.onNotificationAddedToChannel, + this.onNotificationMessageNew = event_handler.onNotificationMessageNew, + this.onNotificationRemovedFromChannel = + event_handler.onNotificationRemovedFromChannel, + this.onUserPresenceChanged = event_handler.onUserPresenceChanged, }) : super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. @@ -18,9 +43,22 @@ class StreamChannelListController extends PagedValueNotifier { required this.client, this.filter, this.sort, - this.limit = 2, + this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, + this.onChannelDeleted = event_handler.onChannelDeleted, + this.onChannelHidden = event_handler.onChannelHidden, + this.onChannelTruncated = event_handler.onChannelTruncated, + this.onChannelUpdated = event_handler.onChannelUpdated, + this.onChannelVisible = event_handler.onChannelVisible, + this.onConnectionRecovered = event_handler.onConnectionRecovered, + this.onMessageNew = event_handler.onMessageNew, + this.onNotificationAddedToChannel = + event_handler.onNotificationAddedToChannel, + this.onNotificationMessageNew = event_handler.onNotificationMessageNew, + this.onNotificationRemovedFromChannel = + event_handler.onNotificationRemovedFromChannel, + this.onUserPresenceChanged = event_handler.onUserPresenceChanged, }) : super(value); /// The client to use for the channel list. @@ -41,6 +79,82 @@ class StreamChannelListController extends PagedValueNotifier { /// The limit to apply to the member list. final int? memberLimit; + /// Callback function which gets called for the event + /// [EventType.channelDeleted]. + /// + /// By default, calls [event_handler.onChannelDeleted] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelDeleted; + + /// Callback function which gets called for the event + /// [EventType.channelHidden]. + /// + /// By default, calls [event_handler.onChannelHidden] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelHidden; + + /// Callback function which gets called for the event + /// [EventType.channelTruncated]. + /// + /// By default, calls [event_handler.onChannelTruncated] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelTruncated; + + /// Callback function which gets called for the event + /// [EventType.channelUpdated]. + /// + /// By default, calls [event_handler.onChannelUpdated] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelUpdated; + + /// Callback function which gets called for the event + /// [EventType.channelVisible]. + /// + /// By default, calls [event_handler.onChannelVisible] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onChannelVisible; + + /// Callback function which gets called for the event + /// [EventType.connectionRecovered]. + /// + /// By default, calls [event_handler.onConnectionRecovered] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onConnectionRecovered; + + /// Callback function which gets called for the event [EventType.messageNew]. + /// + /// By default, calls [event_handler.onMessageNew] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onMessageNew; + + /// Callback function which gets called for the event + /// [EventType.notificationAddedToChannel]. + /// + /// By default, calls [event_handler.onNotificationAddedToChannel] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onNotificationAddedToChannel; + + /// Callback function which gets called for the event + /// [EventType.notificationMessageNew]. + /// + /// By default, calls [event_handler.onNotificationMessageNew] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onNotificationMessageNew; + + /// Callback function which gets called for the event + /// [EventType.notificationRemovedFromChannel]. + /// + /// By default, calls [event_handler.onNotificationRemovedFromChannel] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onNotificationRemovedFromChannel; + + /// Callback function which gets called for the event + /// 'user.presence.changed' and [EventType.userUpdated]. + /// + /// By default, calls [event_handler.onUserPresenceChanged] + /// with the [Event] and the [StreamChannelListController]. + final ChannelListEventHandler onUserPresenceChanged; + @override Future doInitialLoad() async { final limit = this.limit * defaultInitialPagedLimitMultiplier; @@ -58,15 +172,16 @@ class StreamChannelListController extends PagedValueNotifier { nextPageKey: nextKey, ); } - } catch (error) { - value = PagedValue.error(StreamChatError('error')); + // start listening events + _subscribeToChannelListEvents(); + } on StreamChatError catch (error) { + value = PagedValue.error(error); } } @override Future loadMore(int nextPageKey) async { - assert(value is Success, ''); - final previousValue = value as Success; + final previousValue = value.asSuccess; try { await for (final channels in client.queryChannels( @@ -84,8 +199,90 @@ class StreamChannelListController extends PagedValueNotifier { nextPageKey: nextKey, ); } - } catch (error) { - value = previousValue.copyWith(error: StreamChatError('error')); + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); } } + + /// Replaces the previously loaded channels with [channels] and updates + /// the nextPageKey. + set channels(List channels) { + value = PagedValue( + items: channels, + nextPageKey: channels.length, + ); + } + + /// Returns/Creates a new Channel and starts watching it. + Future getChannel({ + required String id, + required String type, + }) async { + final channel = client.channel(type, id: id); + await channel.watch(); + return channel; + } + + StreamSubscription? _channelEventSubscription; + + // Subscribes to the channel list events. + void _subscribeToChannelListEvents() { + if (_channelEventSubscription != null) { + _unsubscribeFromChannelListEvents(); + } + + _channelEventSubscription = client.on().listen((event) { + final eventType = event.type; + if (eventType == EventType.channelDeleted) { + onChannelDeleted(event, this); + } else if (eventType == EventType.channelHidden) { + onChannelHidden(event, this); + } else if (eventType == EventType.channelTruncated) { + onChannelTruncated(event, this); + } else if (eventType == EventType.channelUpdated) { + onChannelUpdated(event, this); + } else if (eventType == EventType.channelVisible) { + onChannelVisible(event, this); + } else if (eventType == EventType.connectionRecovered) { + onConnectionRecovered(event, this); + } else if (eventType == EventType.connectionChanged) { + if (event.online != null) onConnectionRecovered(event, this); + } else if (eventType == EventType.messageNew) { + onMessageNew(event, this); + } else if (eventType == EventType.notificationAddedToChannel) { + onNotificationAddedToChannel(event, this); + } else if (eventType == EventType.notificationMessageNew) { + onNotificationMessageNew(event, this); + } else if (eventType == EventType.notificationRemovedFromChannel) { + onNotificationRemovedFromChannel(event, this); + } else if (eventType == 'user.presence.changed' || + eventType == EventType.userUpdated) { + onUserPresenceChanged(event, this); + } + }); + } + + // Unsubscribes from all channel list events. + void _unsubscribeFromChannelListEvents() { + if (_channelEventSubscription != null) { + _channelEventSubscription!.cancel(); + _channelEventSubscription = null; + } + } + + /// Pauses all subscriptions added to this composite. + void pauseEventsSubscription([Future? resumeSignal]) { + _channelEventSubscription?.pause(resumeSignal); + } + + /// Resumes all subscriptions added to this composite. + void resumeEventsSubscription() { + _channelEventSubscription?.resume(); + } + + @override + void dispose() { + _unsubscribeFromChannelListEvents(); + super.dispose(); + } } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart new file mode 100644 index 00000000..e35549a9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart @@ -0,0 +1,311 @@ +import 'package:stream_chat/stream_chat.dart' show Event; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Handles [EventType.channelDeleted] event. +/// +/// This event is fired when a channel is deleted. +/// +/// By default, this removes the channel from the list of channels. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelDeleted: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelDeleted( + Event event, + StreamChannelListController controller, +) { + final channels = [...controller.currentItems]; + + final updatedChannels = channels + ..removeWhere( + (it) => it.cid == (event.cid ?? event.channel?.cid), + ); + + controller.channels = updatedChannels; +} + +/// Handles [EventType.channelHidden] event. +/// +/// This event is fired when a channel is hidden. +/// +/// By default, this removes the channel from the list of channels. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelHidden: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelHidden( + Event event, + StreamChannelListController controller, +) { + onChannelDeleted(event, controller); +} + +/// Handles [EventType.channelTruncated] event. +/// +/// This event is fired when a channel is truncated. +/// +/// By default, this refreshes the whole channel list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelTruncated: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelTruncated( + Event event, + StreamChannelListController controller, +) { + controller.refresh(); +} + +/// Handles [EventType.channelUpdated] event. +/// +/// This event is fired when a channel is updated. +/// +/// By default, this updates the channel received in the event. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelUpdated: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelUpdated( + Event event, + StreamChannelListController controller, +) { + final eventChannel = event.channel; + if (eventChannel == null) return; + + final channels = [...controller.currentItems]; + final channelIndex = channels.indexWhere( + (it) => it.cid == (event.cid ?? eventChannel.cid), + ); + + if (channelIndex >= 0) { + final channelState = ChannelState(channel: eventChannel); + channels[channelIndex].state?.updateChannelState(channelState); + } + + controller.channels = channels; +} + +/// Handles [EventType.channelVisible] event. +/// +/// This event is fired when a channel is made visible. +/// +/// By default, this adds the channel to the list of channels. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onChannelVisible: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onChannelVisible( + Event event, + StreamChannelListController controller, +) async { + final channelId = event.channelId; + final channelType = event.channelType; + + if (channelId == null || channelType == null) return; + + final channel = await controller.getChannel( + id: channelId, + type: channelType, + ); + + final currentChannels = [...controller.currentItems]; + + final updatedChannels = [ + channel, + ...currentChannels..removeWhere((it) => it.cid == channel.cid), + ]; + + controller.channels = updatedChannels; +} + +/// Handles [EventType.connectionRecovered] event. +/// +/// This event is fired when the client web-socket connection recovers. +/// +/// By default, this refreshes the whole channel list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onConnectionRecovered: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onConnectionRecovered( + Event event, + StreamChannelListController controller, +) { + controller.refresh(); +} + +/// Handles [EventType.messageNew] event. +/// +/// This event is fired when a new message is created in one of the channels +/// we are currently watching. +/// +/// By default, this moves the channel to the top of the list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onMessageNew: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onMessageNew( + Event event, + StreamChannelListController controller, +) { + final channelCid = event.cid; + if (channelCid == null) return; + + final channels = [...controller.currentItems]; + + final channelIndex = channels.indexWhere((it) => it.cid == channelCid); + if (channelIndex <= 0) return; + + final channel = channels.removeAt(channelIndex); + channels.insert(0, channel); + + controller.channels = [...channels]; +} + +/// Handles [EventType.notificationAddedToChannel] event. +/// +/// This event is fired when a channel is added which we are not watching. +/// +/// By default, this adds the channel and moves it to the top of list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onNotificationAddedToChannel: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onNotificationAddedToChannel( + Event event, + StreamChannelListController controller, +) { + onChannelVisible(event, controller); +} + +/// Handles [EventType.notificationMessageNew] event. +/// +/// This event is fired when a new message is created in a channel which we are +/// not currently watching. +/// +/// By default, this adds the channel and moves it to the top of list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onNotificationMessageNew: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onNotificationMessageNew( + Event event, + StreamChannelListController controller, +) { + onChannelVisible(event, controller); +} + +/// Handles [EventType.notificationRemovedFromChannel] event. +/// +/// This event is fired when a user is removed from a channel which we are +/// not currently watching. +/// +/// By default, this removes the event channel from the list. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onNotificationRemovedFromChannel: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onNotificationRemovedFromChannel( + Event event, + StreamChannelListController controller, +) { + final channels = [...controller.currentItems]; + final updatedChannels = channels.where((it) => it.cid != event.channel?.cid); + final listChanged = channels.length != updatedChannels.length; + + if (!listChanged) return; + + controller.channels = [...updatedChannels]; +} + +/// Handles 'user.presence.changed' and [EventType.userUpdated] event. +/// +/// This event is fired when a user's presence changes or gets updated. +/// +/// By default, this updates the channel member with the event user. +/// +/// ```dart +/// StreamChannelListController( +/// client: client, +/// onUserPresenceChanged: (event, controller) { +/// // Do something +/// }, +/// ); +/// ``` +void onUserPresenceChanged( + Event event, + StreamChannelListController controller, +) { + final user = event.user; + if (user == null) return; + + final channels = [...controller.currentItems]; + + final updatedChannels = channels.map((channel) { + final members = [...channel.state!.members]; + final memberIndex = members.indexWhere( + (it) => user.id == (it.userId ?? it.user?.id), + ); + + if (memberIndex < 0) return channel; + + members[memberIndex] = members[memberIndex].copyWith(user: user); + final updatedState = ChannelState(members: [...members]); + channel.state!.updateChannelState(updatedState); + + return channel; + }); + + controller.channels = [...updatedChannels]; +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index ea9f5c1e..b774eaea 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -10,32 +10,44 @@ import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -/// Signature for a function that creates a widget for a given index, e.g., in a -/// list. -/// -/// Used by [GridView.builder] and other APIs that use lazily-generated widgets. -/// -/// See also: -/// -/// * [WidgetBuilder], which is similar but only takes a [BuildContext]. -/// * [TransitionBuilder], which is similar but also takes a child. -/// * [NullableIndexedWidgetBuilder], which is similar but may return null. +Widget defaultSeparatorBuilder(context, index) => + const StreamChannelListSeparator(); + typedef StreamChannelListViewItemBuilder = Widget Function( BuildContext context, Channel channel, ); -typedef StreamChannelTapCallback = void Function(Channel); - -Widget _defaultSeparatorBuilder(context, index) => - const _ChannelListSeparator(); - +/// A [ListView] that shows a list of [Channel]s, +/// it uses [StreamChannelListTile] as a default item. +/// +/// This is the new version of [ChannelListView] that uses +/// [StreamChannelListController]. +/// +/// Example: +/// +/// ```dart +/// StreamChannelListView( +/// controller: controller, +/// onChannelTap: (channel) { +/// // Handle channel tap event +/// }, +/// onChannelLongPress: (channel) { +/// // Handle channel long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamChannelListTile] +/// * [StreamChannelListController] class StreamChannelListView extends StatefulWidget { + /// Creates a new instance of [StreamChannelListView]. const StreamChannelListView({ Key? key, required this.controller, this.itemBuilder, - this.separatorBuilder = _defaultSeparatorBuilder, + this.separatorBuilder = defaultSeparatorBuilder, this.onChannelTap, this.onChannelLongPress, this.padding, @@ -51,21 +63,23 @@ class StreamChannelListView extends StatefulWidget { this.restorationId, }) : super(key: key); + /// The [StreamChannelListController] used to control the list of channels. final StreamChannelListController controller; + /// A builder that is called to build items in the [ListView]. + /// + /// The `index` parameter is the index of the list tile in the list and the + /// `channel` parameter is the [Channel] at that position. final StreamChannelListViewItemBuilder? itemBuilder; + /// A builder that is called to build the list separator. final IndexedWidgetBuilder separatorBuilder; /// Called when the user taps this list tile. - /// - /// Inoperative if [enabled] is false. - final StreamChannelTapCallback? onChannelTap; + final void Function(Channel)? onChannelTap; /// Called when the user long-presses on this list tile. - /// - /// Inoperative if [enabled] is false. - final StreamChannelTapCallback? onChannelLongPress; + final void Function(Channel)? onChannelLongPress; /// The amount of space by which to inset the children. final EdgeInsetsGeometry? padding; @@ -254,11 +268,11 @@ class _StreamChannelListViewState extends State { final newPageRequestTriggerIndex = value.itemCount - 3; final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex; - if (value.hasNextPage && isBuildingTriggerIndexItem) { + if (nextPageKey != null && isBuildingTriggerIndexItem) { // Schedules the request for the end of this frame. WidgetsBinding.instance?.addPostFrameCallback((_) async { if (!value.hasError) { - await _controller.loadMore(nextPageKey!); + await _controller.loadMore(nextPageKey); } _hasRequestedNextPage = false; }); @@ -267,15 +281,15 @@ class _StreamChannelListViewState extends State { } if (index == channels.length) { - if (value.hasError) { - return _ChannelListLoadMoreError( + if (error != null) { + return ChannelListLoadMoreError( onTap: _controller.retry, ); } return const Center( child: Padding( padding: EdgeInsets.all(16), - child: _ChannelListLoadMoreIndicator(), + child: ChannelListLoadMoreIndicator(), ), ); } @@ -309,8 +323,12 @@ class _StreamChannelListViewState extends State { ); } -class _ChannelListLoadMoreIndicator extends StatelessWidget { - const _ChannelListLoadMoreIndicator({Key? key}) : super(key: key); +/// A [StreamChannelListTile] that can be used in a [ListView] to show a +/// loading tile while waiting for the [StreamChannelListController] to load +/// more channels. +class ChannelListLoadMoreIndicator extends StatelessWidget { + /// Creates a new instance of [ChannelListLoadMoreIndicator]. + const ChannelListLoadMoreIndicator({Key? key}) : super(key: key); @override Widget build(BuildContext context) => const SizedBox( @@ -320,12 +338,16 @@ class _ChannelListLoadMoreIndicator extends StatelessWidget { ); } -class _ChannelListLoadMoreError extends StatelessWidget { - const _ChannelListLoadMoreError({ +/// A [StreamChannelListTile] that is used to display the error indicator when +/// loading more channels fails. +class ChannelListLoadMoreError extends StatelessWidget { + /// Creates a new instance of [ChannelListLoadMoreError]. + const ChannelListLoadMoreError({ Key? key, required this.onTap, }) : super(key: key); + /// The callback to invoke when the user taps on the error indicator. final GestureTapCallback onTap; @override @@ -355,8 +377,11 @@ class _ChannelListLoadMoreError extends StatelessWidget { } } -class _ChannelListSeparator extends StatelessWidget { - const _ChannelListSeparator({Key? key}) : super(key: key); +/// A widget that is used to display a separator between +/// [StreamChannelListTile] items. +class StreamChannelListSeparator extends StatelessWidget { + /// Creates a new instance of [StreamChannelListSeparator]. + const StreamChannelListSeparator({Key? key}) : super(key: key); @override Widget build(BuildContext context) { From 6466d58156269ca5936a37396f746d5a271e1c0e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Nov 2021 18:01:16 +0530 Subject: [PATCH 028/112] chore(ui): add docs Signed-off-by: xsahil03x --- .../stream_channel_list_loading_tile.dart | 5 ++ .../stream_channel_list_tile.dart | 53 ++++++++----------- .../stream_channel_list_view.dart | 9 ++-- .../lib/src/v4/stream_channel_name.dart | 1 + 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart index 9e0231d5..69a6cf6e 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart @@ -2,7 +2,12 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// A shimmering list item which shows a loading effect. +/// +/// This is used by [StreamChannelListView] to show a loading effect while +/// the list is being loaded. class StreamChannelListLoadingTile extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadingTile] widget. const StreamChannelListLoadingTile({ Key? key, this.visualDensity = VisualDensity.standard, diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index f4fb4fde..a3d0f67a 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat/stream_chat.dart' show Channel; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/sending_indicator.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/theme/channel_preview_theme.dart'; @@ -10,9 +11,20 @@ import 'package:stream_chat_flutter/src/unread_indicator.dart'; import 'package:stream_chat_flutter/src/v4/stream_channel_avatar.dart'; import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; +/// A widget that displays a channel preview. +/// +/// This widget is intended to be used as a Tile in [StreamChannelListView] +/// +/// It shows the last message of the channel, the last message time, the unread +/// message count, the typing indicator, the sending indicator and the channel +/// avatar. +/// +/// See also: +/// * [StreamChannelAvatar] +/// * [StreamChannelName] class StreamChannelListTile extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTile] widget. StreamChannelListTile({ Key? key, required this.channel, @@ -29,50 +41,22 @@ class StreamChannelListTile extends StatelessWidget { ), super(key: key); + /// The channel to display. final Channel channel; /// A widget to display before the title. - /// - /// Typically an [Icon] or a [CircleAvatar] widget. final Widget? leading; /// The primary content of the list tile. - /// - /// Typically a [Text] widget. - /// - /// This should not wrap. To enforce the single line limit, use - /// [Text.maxLines]. final Widget? title; /// Additional content displayed below the title. - /// - /// Typically a [Text] widget. - /// - /// If [isThreeLine] is false, this should not wrap. - /// - /// If [isThreeLine] is true, this should be configured to take a maximum of - /// two lines. For example, you can use [Text.maxLines] to enforce the number - /// of lines. - /// - /// The subtitle's default [TextStyle] depends on [TextTheme.bodyText2] except - /// [TextStyle.color]. The [TextStyle.color] depends on the value of [enabled] - /// and [selected]. - /// - /// When [enabled] is false, the text color is set to [ThemeData.disabledColor]. - /// - /// When [selected] is false, the text color is set to [ListTileTheme.textColor] - /// if it's not null and to [TextTheme.caption]'s color if [ListTileTheme.textColor] - /// is null. final Widget? subtitle; /// Called when the user taps this list tile. - /// - /// Inoperative if [enabled] is false. final GestureTapCallback? onTap; /// Called when the user long-presses on this list tile. - /// - /// Inoperative if [enabled] is false. final GestureLongPressCallback? onLongPress; /// Defines how compact the list tile's layout will be. @@ -196,7 +180,9 @@ class StreamChannelListTile extends StatelessWidget { } } +/// A widget that displays the channel last message date. class ChannelLastMessageDate extends StatelessWidget { + /// Creates a new instance of the [ChannelLastMessageDate] widget. ChannelLastMessageDate({ Key? key, required this.channel, @@ -207,6 +193,7 @@ class ChannelLastMessageDate extends StatelessWidget { ), super(key: key); + /// The channel to display the last message date for. final Channel channel; /// The style of the text displayed @@ -246,7 +233,9 @@ class ChannelLastMessageDate extends StatelessWidget { ); } +/// A widget that displays the subtitle for [StreamChannelListTile]. class ChannelListTileSubtitle extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTileSubtitle] widget. ChannelListTileSubtitle({ Key? key, required this.channel, @@ -257,6 +246,7 @@ class ChannelListTileSubtitle extends StatelessWidget { ), super(key: key); + /// The channel to create the subtitle from. final Channel channel; /// The style of the text displayed @@ -287,7 +277,9 @@ class ChannelListTileSubtitle extends StatelessWidget { } } +/// A widget that displays the last message of a channel. class ChannelLastMessageText extends StatelessWidget { + /// Creates a new instance of [ChannelLastMessageText] widget. ChannelLastMessageText({ Key? key, required this.channel, @@ -298,6 +290,7 @@ class ChannelLastMessageText extends StatelessWidget { ), super(key: key); + /// The channel to display the last message of. final Channel channel; /// The style of the text displayed diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index b774eaea..af493815 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -10,9 +10,12 @@ import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -Widget defaultSeparatorBuilder(context, index) => +/// Default separator builder for [StreamChannelListView]. +Widget defaultSeparatorBuilder(BuildContext context, int index) => const StreamChannelListSeparator(); +/// Signature for the item builder that creates the children of the +/// [StreamChannelListView]. typedef StreamChannelListViewItemBuilder = Widget Function( BuildContext context, Channel channel, @@ -265,13 +268,13 @@ class _StreamChannelListViewState extends State { separatorBuilder: widget.separatorBuilder, itemBuilder: (context, index) { if (!_hasRequestedNextPage) { - final newPageRequestTriggerIndex = value.itemCount - 3; + final newPageRequestTriggerIndex = channels.length - 3; final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex; if (nextPageKey != null && isBuildingTriggerIndexItem) { // Schedules the request for the end of this frame. WidgetsBinding.instance?.addPostFrameCallback((_) async { - if (!value.hasError) { + if (error == null) { await _controller.loadMore(nextPageKey); } _hasRequestedNextPage = false; diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart index 2bcd56e8..7d3be9a7 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart @@ -21,6 +21,7 @@ class StreamChannelName extends StatelessWidget { ), super(key: key); + /// The [Channel] to show the name for. final Channel channel; /// The style of the text displayed From 430a92375a58f3f5b2a8079d16e14d3f2b77fa21 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 29 Nov 2021 16:40:44 +0530 Subject: [PATCH 029/112] chore(ui): add support for presence. Signed-off-by: xsahil03x --- .../stream_channel_list_controller.dart | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index d4715619..de6196cd 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -12,13 +12,14 @@ typedef ChannelListEventHandler = void Function( StreamChannelListController controller, ); -/// A controller for the channel list view. +/// class StreamChannelListController extends PagedValueNotifier { - /// Creates a [StreamChannelListController]. + /// Creates a new instance of [StreamChannelListController]. StreamChannelListController({ required this.client, this.filter, this.sort, + this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, @@ -43,6 +44,7 @@ class StreamChannelListController extends PagedValueNotifier { required this.client, this.filter, this.sort, + this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, @@ -64,21 +66,31 @@ class StreamChannelListController extends PagedValueNotifier { /// The client to use for the channel list. final StreamChatClient client; - /// The filter to apply to the channel list. + /// The query filters to use. + /// You can query on any of the custom fields you've defined on the [Channel]. + /// You can also filter other built-in channel fields. final Filter? filter; - /// The sort to apply to the channel list. + /// The sorting used for the channels matching the filters. + /// Sorting is based on field and direction, multiple sorting options + /// can be provided. + /// You can sort based on last_updated, last_message_at, updated_at, + /// created_at or member_count. + /// Direction can be ascending or descending. final List>? sort; + /// If true you’ll receive user presence updates via the websocket events + final bool presence; + /// The limit to apply to the channel list. final int limit; - /// The limit to apply to the message list. - final int? messageLimit; - - /// The limit to apply to the member list. + /// Number of members to fetch in each channel final int? memberLimit; + /// Number of messages to fetch in each channel + final int? messageLimit; + /// Callback function which gets called for the event /// [EventType.channelDeleted]. /// @@ -164,6 +176,7 @@ class StreamChannelListController extends PagedValueNotifier { sort: sort, memberLimit: memberLimit, messageLimit: messageLimit, + presence: presence, paginationParams: PaginationParams(limit: limit), )) { final nextKey = channels.length < limit ? null : channels.length; @@ -176,6 +189,9 @@ class StreamChannelListController extends PagedValueNotifier { _subscribeToChannelListEvents(); } on StreamChatError catch (error) { value = PagedValue.error(error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = PagedValue.error(chatError); } } @@ -189,6 +205,7 @@ class StreamChannelListController extends PagedValueNotifier { sort: sort, memberLimit: memberLimit, messageLimit: messageLimit, + presence: presence, paginationParams: PaginationParams(limit: limit, offset: nextPageKey), )) { final previousItems = previousValue.items; @@ -201,6 +218,9 @@ class StreamChannelListController extends PagedValueNotifier { } } on StreamChatError catch (error) { value = previousValue.copyWith(error: error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = previousValue.copyWith(error: chatError); } } From 042315353ced6e541a202ff1e145af3a4a851fc7 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Thu, 2 Dec 2021 12:34:58 +0100 Subject: [PATCH 030/112] refactor: add ChannelEvents class and remove event callbacks --- .../example/lib/tutorial_part_2.dart | 2 +- .../v4/channel_list_view/channel_events.dart | 123 +++++++++++ .../stream_channel_list_controller.dart | 191 ++++++------------ .../lib/stream_chat_flutter.dart | 5 +- 4 files changed, 188 insertions(+), 133 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 6e1edaba..f704abab 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -70,7 +70,7 @@ class MyApp extends StatelessWidget { } class ChannelListPage extends StatefulWidget { - ChannelListPage({ + const ChannelListPage({ Key? key, required this.client, }) : super(key: key); diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart new file mode 100644 index 00000000..05c2bdbd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart @@ -0,0 +1,123 @@ +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' + as event_handler; + +/// Contains methods that are called for certain [Event]s. These methods are +/// called from the [StreamChannelListController]. +/// +/// This class can be mixed in or extended to create custom overrides. +class ChannelEvents { + /// Function which gets called for the event + /// [EventType.channelDeleted]. + /// + /// By default, calls [event_handler.onChannelDeleted] + /// with the [Event] and the [StreamChannelListController]. + void onChannelDeleted(Event event, StreamChannelListController controller) { + event_handler.onChannelDeleted(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelHidden]. + /// + /// By default, calls [event_handler.onChannelHidden] + /// with the [Event] and the [StreamChannelListController]. + void onChannelHidden(Event event, StreamChannelListController controller) { + event_handler.onChannelHidden(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelTruncated]. + /// + /// By default, calls [event_handler.onChannelTruncated] + /// with the [Event] and the [StreamChannelListController]. + void onChannelTruncated(Event event, StreamChannelListController controller) { + event_handler.onChannelTruncated(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelUpdated]. + /// + /// By default, calls [event_handler.onChannelUpdated] + /// with the [Event] and the [StreamChannelListController]. + void onChannelUpdated(Event event, StreamChannelListController controller) { + event_handler.onChannelUpdated(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelVisible]. + /// + /// By default, calls [event_handler.onChannelVisible] + /// with the [Event] and the [StreamChannelListController]. + void onChannelVisible(Event event, StreamChannelListController controller) { + event_handler.onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.connectionRecovered]. + /// + /// By default, calls [event_handler.onConnectionRecovered] + /// with the [Event] and the [StreamChannelListController]. + void onConnectionRecovered( + Event event, + StreamChannelListController controller, + ) { + event_handler.onConnectionRecovered(event, controller); + } + + /// Function which gets called for the event [EventType.messageNew]. + /// + /// By default, calls [event_handler.onMessageNew] + /// with the [Event] and the [StreamChannelListController]. + void onMessageNew(Event event, StreamChannelListController controller) { + event_handler.onMessageNew(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationAddedToChannel]. + /// + /// By default, calls [event_handler.onNotificationAddedToChannel] + /// with the [Event] and the [StreamChannelListController]. + void onNotificationAddedToChannel( + Event event, + StreamChannelListController controller, + ) { + event_handler.onNotificationAddedToChannel(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationMessageNew]. + /// + /// By default, calls [event_handler.onNotificationMessageNew] + /// with the [Event] and the [StreamChannelListController]. + void onNotificationMessageNew( + Event event, + StreamChannelListController controller, + ) { + event_handler.onNotificationMessageNew(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationRemovedFromChannel]. + /// + /// By default, calls [event_handler.onNotificationRemovedFromChannel] + /// with the [Event] and the [StreamChannelListController]. + void onNotificationRemovedFromChannel( + Event event, + StreamChannelListController controller, + ) { + event_handler.onNotificationRemovedFromChannel(event, controller); + } + + /// Function which gets called for the event + /// 'user.presence.changed' and [EventType.userUpdated]. + /// + /// By default, calls [event_handler.onUserPresenceChanged] + /// with the [Event] and the [StreamChannelListController]. + void onUserPresenceChanged( + Event event, + StreamChannelListController controller, + ) { + event_handler.onUserPresenceChanged(event, controller); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index de6196cd..3ea6a614 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -2,68 +2,72 @@ import 'dart:async'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' - as event_handler; +import 'package:stream_chat_flutter/src/v4/channel_list_view/channel_events.dart'; +/// The default channel page limit to load. const defaultChannelPagedLimit = 10; -typedef ChannelListEventHandler = void Function( - Event event, - StreamChannelListController controller, -); - -/// +/// A controller for a Channel list. +/// +/// This class lets you perform tasks such as: +/// * Load initial data. +/// * Load more data using [loadMore]. +/// * Replace the previously loaded channels. +/// * Return/Create a new channel and start watching it. +/// * Unsubscribe from all channel list events. +/// * Pause and Resume all subscriptions added to this composite. class StreamChannelListController extends PagedValueNotifier { - /// Creates a new instance of [StreamChannelListController]. + /// Creates a Stream channel list controller. + /// + /// * `client` is the Stream chat client to use for the channels list. + /// + /// * `channelEvents` is the channel events to use for the channels list. + /// This class can be mixed in or extended to create custom overrides. See + /// [ChannelEvents] for advice. + /// + /// * `filter` is the query filters to use. + /// + /// * `sort` is the sorting used for the channels matching the filters. + /// + /// * `presence` sets whether you'll receive user presence updates via the + /// websocket events. + /// + /// * `limit` is the limit to apply to the channel list. + /// + /// * `messageLimit` is the number of messages to fetch in each channel. + /// + /// * `memberLimit` is the number of members to fetch in each channel. StreamChannelListController({ required this.client, + ChannelEvents? channelEvents, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - this.onChannelDeleted = event_handler.onChannelDeleted, - this.onChannelHidden = event_handler.onChannelHidden, - this.onChannelTruncated = event_handler.onChannelTruncated, - this.onChannelUpdated = event_handler.onChannelUpdated, - this.onChannelVisible = event_handler.onChannelVisible, - this.onConnectionRecovered = event_handler.onConnectionRecovered, - this.onMessageNew = event_handler.onMessageNew, - this.onNotificationAddedToChannel = - event_handler.onNotificationAddedToChannel, - this.onNotificationMessageNew = event_handler.onNotificationMessageNew, - this.onNotificationRemovedFromChannel = - event_handler.onNotificationRemovedFromChannel, - this.onUserPresenceChanged = event_handler.onUserPresenceChanged, - }) : super(const PagedValue.loading()); + }) : channelEvents = channelEvents ?? ChannelEvents(), + super(const PagedValue.loading()) { + this.channelEvents.test(); + } /// Creates a [StreamChannelListController] from the passed [value]. StreamChannelListController.fromValue( PagedValue value, { required this.client, + required this.channelEvents, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - this.onChannelDeleted = event_handler.onChannelDeleted, - this.onChannelHidden = event_handler.onChannelHidden, - this.onChannelTruncated = event_handler.onChannelTruncated, - this.onChannelUpdated = event_handler.onChannelUpdated, - this.onChannelVisible = event_handler.onChannelVisible, - this.onConnectionRecovered = event_handler.onConnectionRecovered, - this.onMessageNew = event_handler.onMessageNew, - this.onNotificationAddedToChannel = - event_handler.onNotificationAddedToChannel, - this.onNotificationMessageNew = event_handler.onNotificationMessageNew, - this.onNotificationRemovedFromChannel = - event_handler.onNotificationRemovedFromChannel, - this.onUserPresenceChanged = event_handler.onUserPresenceChanged, }) : super(value); - /// The client to use for the channel list. + /// The channel events to use for the channels list. + final ChannelEvents channelEvents; + + /// The client to use for the channels list. final StreamChatClient client; /// The query filters to use. @@ -82,90 +86,15 @@ class StreamChannelListController extends PagedValueNotifier { /// If true you’ll receive user presence updates via the websocket events final bool presence; - /// The limit to apply to the channel list. + /// The limit to apply to the channel list. The default is set to + /// [defaultChannelPagedLimit]. final int limit; - /// Number of members to fetch in each channel - final int? memberLimit; - - /// Number of messages to fetch in each channel + /// Number of messages to fetch in each channel. final int? messageLimit; - /// Callback function which gets called for the event - /// [EventType.channelDeleted]. - /// - /// By default, calls [event_handler.onChannelDeleted] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelDeleted; - - /// Callback function which gets called for the event - /// [EventType.channelHidden]. - /// - /// By default, calls [event_handler.onChannelHidden] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelHidden; - - /// Callback function which gets called for the event - /// [EventType.channelTruncated]. - /// - /// By default, calls [event_handler.onChannelTruncated] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelTruncated; - - /// Callback function which gets called for the event - /// [EventType.channelUpdated]. - /// - /// By default, calls [event_handler.onChannelUpdated] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelUpdated; - - /// Callback function which gets called for the event - /// [EventType.channelVisible]. - /// - /// By default, calls [event_handler.onChannelVisible] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onChannelVisible; - - /// Callback function which gets called for the event - /// [EventType.connectionRecovered]. - /// - /// By default, calls [event_handler.onConnectionRecovered] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onConnectionRecovered; - - /// Callback function which gets called for the event [EventType.messageNew]. - /// - /// By default, calls [event_handler.onMessageNew] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onMessageNew; - - /// Callback function which gets called for the event - /// [EventType.notificationAddedToChannel]. - /// - /// By default, calls [event_handler.onNotificationAddedToChannel] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onNotificationAddedToChannel; - - /// Callback function which gets called for the event - /// [EventType.notificationMessageNew]. - /// - /// By default, calls [event_handler.onNotificationMessageNew] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onNotificationMessageNew; - - /// Callback function which gets called for the event - /// [EventType.notificationRemovedFromChannel]. - /// - /// By default, calls [event_handler.onNotificationRemovedFromChannel] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onNotificationRemovedFromChannel; - - /// Callback function which gets called for the event - /// 'user.presence.changed' and [EventType.userUpdated]. - /// - /// By default, calls [event_handler.onUserPresenceChanged] - /// with the [Event] and the [StreamChannelListController]. - final ChannelListEventHandler onUserPresenceChanged; + /// Number of members to fetch in each channel. + final int? memberLimit; @override Future doInitialLoad() async { @@ -185,7 +114,7 @@ class StreamChannelListController extends PagedValueNotifier { nextPageKey: nextKey, ); } - // start listening events + // start listening to events _subscribeToChannelListEvents(); } on StreamChatError catch (error) { value = PagedValue.error(error); @@ -254,30 +183,32 @@ class StreamChannelListController extends PagedValueNotifier { _channelEventSubscription = client.on().listen((event) { final eventType = event.type; if (eventType == EventType.channelDeleted) { - onChannelDeleted(event, this); + channelEvents.onChannelDeleted(event, this); } else if (eventType == EventType.channelHidden) { - onChannelHidden(event, this); + channelEvents.onChannelDeleted(event, this); } else if (eventType == EventType.channelTruncated) { - onChannelTruncated(event, this); + channelEvents.onChannelTruncated(event, this); } else if (eventType == EventType.channelUpdated) { - onChannelUpdated(event, this); + channelEvents.onChannelUpdated(event, this); } else if (eventType == EventType.channelVisible) { - onChannelVisible(event, this); + channelEvents.onChannelVisible(event, this); } else if (eventType == EventType.connectionRecovered) { - onConnectionRecovered(event, this); + channelEvents.onConnectionRecovered(event, this); } else if (eventType == EventType.connectionChanged) { - if (event.online != null) onConnectionRecovered(event, this); + if (event.online != null) { + channelEvents.onConnectionRecovered(event, this); + } } else if (eventType == EventType.messageNew) { - onMessageNew(event, this); + channelEvents.onMessageNew(event, this); } else if (eventType == EventType.notificationAddedToChannel) { - onNotificationAddedToChannel(event, this); + channelEvents.onNotificationAddedToChannel(event, this); } else if (eventType == EventType.notificationMessageNew) { - onNotificationMessageNew(event, this); + channelEvents.onNotificationMessageNew(event, this); } else if (eventType == EventType.notificationRemovedFromChannel) { - onNotificationRemovedFromChannel(event, this); + channelEvents.onNotificationRemovedFromChannel(event, this); } else if (eventType == 'user.presence.changed' || eventType == EventType.userUpdated) { - onUserPresenceChanged(event, this); + channelEvents.onUserPresenceChanged(event, this); } }); } diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index ce82789a..b1e25b9e 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -47,6 +47,7 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; -export 'src/visible_footnote.dart'; -export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/v4/channel_list_view/channel_events.dart'; export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; +export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/visible_footnote.dart'; From 5b0536349735c224dd78911af6fe819c952f999b Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Thu, 2 Dec 2021 16:50:20 +0100 Subject: [PATCH 031/112] refactor: rename ChannelEvents to ChannelEventHandlers --- ...vents.dart => channel_event_handlers.dart} | 6 +-- .../stream_channel_list_controller.dart | 48 +++++++++---------- .../lib/stream_chat_flutter.dart | 2 +- 3 files changed, 28 insertions(+), 28 deletions(-) rename packages/stream_chat_flutter/lib/src/v4/channel_list_view/{channel_events.dart => channel_event_handlers.dart} (96%) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart similarity index 96% rename from packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart rename to packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart index 05c2bdbd..57b7d8c8 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_events.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart @@ -3,11 +3,11 @@ import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' as event_handler; -/// Contains methods that are called for certain [Event]s. These methods are -/// called from the [StreamChannelListController]. +/// Contains handlers that are called from [StreamChannelListController] for +/// certain [Event]s. /// /// This class can be mixed in or extended to create custom overrides. -class ChannelEvents { +class ChannelEventHandlers { /// Function which gets called for the event /// [EventType.channelDeleted]. /// diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 3ea6a614..73cfa2ee 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/channel_events.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/channel_event_handlers.dart'; /// The default channel page limit to load. const defaultChannelPagedLimit = 10; @@ -11,6 +11,7 @@ const defaultChannelPagedLimit = 10; /// /// This class lets you perform tasks such as: /// * Load initial data. +/// * Use channel events handlers. /// * Load more data using [loadMore]. /// * Replace the previously loaded channels. /// * Return/Create a new channel and start watching it. @@ -21,9 +22,9 @@ class StreamChannelListController extends PagedValueNotifier { /// /// * `client` is the Stream chat client to use for the channels list. /// - /// * `channelEvents` is the channel events to use for the channels list. - /// This class can be mixed in or extended to create custom overrides. See - /// [ChannelEvents] for advice. + /// * `channelEventHandlers` is the channel events to use for the channels + /// list. This class can be mixed in or extended to create custom overrides. + /// See [ChannelEventHandlers] for advice. /// /// * `filter` is the query filters to use. /// @@ -39,33 +40,32 @@ class StreamChannelListController extends PagedValueNotifier { /// * `memberLimit` is the number of members to fetch in each channel. StreamChannelListController({ required this.client, - ChannelEvents? channelEvents, + ChannelEventHandlers? channelEventHandlers, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : channelEvents = channelEvents ?? ChannelEvents(), - super(const PagedValue.loading()) { - this.channelEvents.test(); - } + }) : _channelEventHandlers = channelEventHandlers ?? ChannelEventHandlers(), + super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. StreamChannelListController.fromValue( PagedValue value, { required this.client, - required this.channelEvents, + ChannelEventHandlers? channelEventHandlers, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : super(value); + }) : _channelEventHandlers = channelEventHandlers ?? ChannelEventHandlers(), + super(value); /// The channel events to use for the channels list. - final ChannelEvents channelEvents; + final ChannelEventHandlers _channelEventHandlers; /// The client to use for the channels list. final StreamChatClient client; @@ -183,32 +183,32 @@ class StreamChannelListController extends PagedValueNotifier { _channelEventSubscription = client.on().listen((event) { final eventType = event.type; if (eventType == EventType.channelDeleted) { - channelEvents.onChannelDeleted(event, this); + _channelEventHandlers.onChannelDeleted(event, this); } else if (eventType == EventType.channelHidden) { - channelEvents.onChannelDeleted(event, this); + _channelEventHandlers.onChannelHidden(event, this); } else if (eventType == EventType.channelTruncated) { - channelEvents.onChannelTruncated(event, this); + _channelEventHandlers.onChannelTruncated(event, this); } else if (eventType == EventType.channelUpdated) { - channelEvents.onChannelUpdated(event, this); + _channelEventHandlers.onChannelUpdated(event, this); } else if (eventType == EventType.channelVisible) { - channelEvents.onChannelVisible(event, this); + _channelEventHandlers.onChannelVisible(event, this); } else if (eventType == EventType.connectionRecovered) { - channelEvents.onConnectionRecovered(event, this); + _channelEventHandlers.onConnectionRecovered(event, this); } else if (eventType == EventType.connectionChanged) { if (event.online != null) { - channelEvents.onConnectionRecovered(event, this); + _channelEventHandlers.onConnectionRecovered(event, this); } } else if (eventType == EventType.messageNew) { - channelEvents.onMessageNew(event, this); + _channelEventHandlers.onMessageNew(event, this); } else if (eventType == EventType.notificationAddedToChannel) { - channelEvents.onNotificationAddedToChannel(event, this); + _channelEventHandlers.onNotificationAddedToChannel(event, this); } else if (eventType == EventType.notificationMessageNew) { - channelEvents.onNotificationMessageNew(event, this); + _channelEventHandlers.onNotificationMessageNew(event, this); } else if (eventType == EventType.notificationRemovedFromChannel) { - channelEvents.onNotificationRemovedFromChannel(event, this); + _channelEventHandlers.onNotificationRemovedFromChannel(event, this); } else if (eventType == 'user.presence.changed' || eventType == EventType.userUpdated) { - channelEvents.onUserPresenceChanged(event, this); + _channelEventHandlers.onUserPresenceChanged(event, this); } }); } diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index b1e25b9e..76e3bc0e 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -47,7 +47,7 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; -export 'src/v4/channel_list_view/channel_events.dart'; +export 'src/v4/channel_list_view/channel_event_handlers.dart'; export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; export 'src/visible_footnote.dart'; From d4bde539e6079134fb84d64f18beb5ff8ca1b2bd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 3 Dec 2021 18:52:11 +0530 Subject: [PATCH 032/112] refactor(llc): minor event handler changes. Signed-off-by: xsahil03x --- .../channel_event_handlers.dart | 123 ----- .../stream_channel_list_controller.dart | 48 +- .../stream_channel_list_event_handler.dart | 492 +++++++----------- .../lib/stream_chat_flutter.dart | 2 +- 4 files changed, 224 insertions(+), 441 deletions(-) delete mode 100644 packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart deleted file mode 100644 index 57b7d8c8..00000000 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/channel_event_handlers.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:stream_chat/stream_chat.dart' hide Success; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart' - as event_handler; - -/// Contains handlers that are called from [StreamChannelListController] for -/// certain [Event]s. -/// -/// This class can be mixed in or extended to create custom overrides. -class ChannelEventHandlers { - /// Function which gets called for the event - /// [EventType.channelDeleted]. - /// - /// By default, calls [event_handler.onChannelDeleted] - /// with the [Event] and the [StreamChannelListController]. - void onChannelDeleted(Event event, StreamChannelListController controller) { - event_handler.onChannelDeleted(event, controller); - } - - /// Function which gets called for the event - /// [EventType.channelHidden]. - /// - /// By default, calls [event_handler.onChannelHidden] - /// with the [Event] and the [StreamChannelListController]. - void onChannelHidden(Event event, StreamChannelListController controller) { - event_handler.onChannelHidden(event, controller); - } - - /// Function which gets called for the event - /// [EventType.channelTruncated]. - /// - /// By default, calls [event_handler.onChannelTruncated] - /// with the [Event] and the [StreamChannelListController]. - void onChannelTruncated(Event event, StreamChannelListController controller) { - event_handler.onChannelTruncated(event, controller); - } - - /// Function which gets called for the event - /// [EventType.channelUpdated]. - /// - /// By default, calls [event_handler.onChannelUpdated] - /// with the [Event] and the [StreamChannelListController]. - void onChannelUpdated(Event event, StreamChannelListController controller) { - event_handler.onChannelUpdated(event, controller); - } - - /// Function which gets called for the event - /// [EventType.channelVisible]. - /// - /// By default, calls [event_handler.onChannelVisible] - /// with the [Event] and the [StreamChannelListController]. - void onChannelVisible(Event event, StreamChannelListController controller) { - event_handler.onChannelVisible(event, controller); - } - - /// Function which gets called for the event - /// [EventType.connectionRecovered]. - /// - /// By default, calls [event_handler.onConnectionRecovered] - /// with the [Event] and the [StreamChannelListController]. - void onConnectionRecovered( - Event event, - StreamChannelListController controller, - ) { - event_handler.onConnectionRecovered(event, controller); - } - - /// Function which gets called for the event [EventType.messageNew]. - /// - /// By default, calls [event_handler.onMessageNew] - /// with the [Event] and the [StreamChannelListController]. - void onMessageNew(Event event, StreamChannelListController controller) { - event_handler.onMessageNew(event, controller); - } - - /// Function which gets called for the event - /// [EventType.notificationAddedToChannel]. - /// - /// By default, calls [event_handler.onNotificationAddedToChannel] - /// with the [Event] and the [StreamChannelListController]. - void onNotificationAddedToChannel( - Event event, - StreamChannelListController controller, - ) { - event_handler.onNotificationAddedToChannel(event, controller); - } - - /// Function which gets called for the event - /// [EventType.notificationMessageNew]. - /// - /// By default, calls [event_handler.onNotificationMessageNew] - /// with the [Event] and the [StreamChannelListController]. - void onNotificationMessageNew( - Event event, - StreamChannelListController controller, - ) { - event_handler.onNotificationMessageNew(event, controller); - } - - /// Function which gets called for the event - /// [EventType.notificationRemovedFromChannel]. - /// - /// By default, calls [event_handler.onNotificationRemovedFromChannel] - /// with the [Event] and the [StreamChannelListController]. - void onNotificationRemovedFromChannel( - Event event, - StreamChannelListController controller, - ) { - event_handler.onNotificationRemovedFromChannel(event, controller); - } - - /// Function which gets called for the event - /// 'user.presence.changed' and [EventType.userUpdated]. - /// - /// By default, calls [event_handler.onUserPresenceChanged] - /// with the [Event] and the [StreamChannelListController]. - void onUserPresenceChanged( - Event event, - StreamChannelListController controller, - ) { - event_handler.onUserPresenceChanged(event, controller); - } -} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 73cfa2ee..5dcbeaa2 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/channel_event_handlers.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart'; /// The default channel page limit to load. const defaultChannelPagedLimit = 10; @@ -15,7 +15,6 @@ const defaultChannelPagedLimit = 10; /// * Load more data using [loadMore]. /// * Replace the previously loaded channels. /// * Return/Create a new channel and start watching it. -/// * Unsubscribe from all channel list events. /// * Pause and Resume all subscriptions added to this composite. class StreamChannelListController extends PagedValueNotifier { /// Creates a Stream channel list controller. @@ -24,7 +23,7 @@ class StreamChannelListController extends PagedValueNotifier { /// /// * `channelEventHandlers` is the channel events to use for the channels /// list. This class can be mixed in or extended to create custom overrides. - /// See [ChannelEventHandlers] for advice. + /// See [StreamChannelListEventHandler] for advice. /// /// * `filter` is the query filters to use. /// @@ -40,46 +39,51 @@ class StreamChannelListController extends PagedValueNotifier { /// * `memberLimit` is the number of members to fetch in each channel. StreamChannelListController({ required this.client, - ChannelEventHandlers? channelEventHandlers, + StreamChannelListEventHandler? eventHandler, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : _channelEventHandlers = channelEventHandlers ?? ChannelEventHandlers(), + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. StreamChannelListController.fromValue( PagedValue value, { required this.client, - ChannelEventHandlers? channelEventHandlers, + StreamChannelListEventHandler? eventHandler, this.filter, this.sort, this.presence = true, this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : _channelEventHandlers = channelEventHandlers ?? ChannelEventHandlers(), + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), super(value); - /// The channel events to use for the channels list. - final ChannelEventHandlers _channelEventHandlers; - /// The client to use for the channels list. final StreamChatClient client; + /// The channel event handlers to use for the channels list. + final StreamChannelListEventHandler _eventHandler; + /// The query filters to use. + /// /// You can query on any of the custom fields you've defined on the [Channel]. + /// /// You can also filter other built-in channel fields. final Filter? filter; /// The sorting used for the channels matching the filters. + /// /// Sorting is based on field and direction, multiple sorting options /// can be provided. + /// /// You can sort based on last_updated, last_message_at, updated_at, /// created_at or member_count. + /// /// Direction can be ascending or descending. final List>? sort; @@ -183,32 +187,32 @@ class StreamChannelListController extends PagedValueNotifier { _channelEventSubscription = client.on().listen((event) { final eventType = event.type; if (eventType == EventType.channelDeleted) { - _channelEventHandlers.onChannelDeleted(event, this); + _eventHandler.onChannelDeleted(event, this); } else if (eventType == EventType.channelHidden) { - _channelEventHandlers.onChannelHidden(event, this); + _eventHandler.onChannelHidden(event, this); } else if (eventType == EventType.channelTruncated) { - _channelEventHandlers.onChannelTruncated(event, this); + _eventHandler.onChannelTruncated(event, this); } else if (eventType == EventType.channelUpdated) { - _channelEventHandlers.onChannelUpdated(event, this); + _eventHandler.onChannelUpdated(event, this); } else if (eventType == EventType.channelVisible) { - _channelEventHandlers.onChannelVisible(event, this); + _eventHandler.onChannelVisible(event, this); } else if (eventType == EventType.connectionRecovered) { - _channelEventHandlers.onConnectionRecovered(event, this); + _eventHandler.onConnectionRecovered(event, this); } else if (eventType == EventType.connectionChanged) { if (event.online != null) { - _channelEventHandlers.onConnectionRecovered(event, this); + _eventHandler.onConnectionRecovered(event, this); } } else if (eventType == EventType.messageNew) { - _channelEventHandlers.onMessageNew(event, this); + _eventHandler.onMessageNew(event, this); } else if (eventType == EventType.notificationAddedToChannel) { - _channelEventHandlers.onNotificationAddedToChannel(event, this); + _eventHandler.onNotificationAddedToChannel(event, this); } else if (eventType == EventType.notificationMessageNew) { - _channelEventHandlers.onNotificationMessageNew(event, this); + _eventHandler.onNotificationMessageNew(event, this); } else if (eventType == EventType.notificationRemovedFromChannel) { - _channelEventHandlers.onNotificationRemovedFromChannel(event, this); + _eventHandler.onNotificationRemovedFromChannel(event, this); } else if (eventType == 'user.presence.changed' || eventType == EventType.userUpdated) { - _channelEventHandlers.onUserPresenceChanged(event, this); + _eventHandler.onUserPresenceChanged(event, this); } }); } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart index e35549a9..bd6cf50d 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart @@ -1,311 +1,213 @@ -import 'package:stream_chat/stream_chat.dart' show Event; +import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// Handles [EventType.channelDeleted] event. +/// Contains handlers that are called from [StreamChannelListController] for +/// certain [Event]s. /// -/// This event is fired when a channel is deleted. -/// -/// By default, this removes the channel from the list of channels. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelDeleted: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelDeleted( - Event event, - StreamChannelListController controller, -) { - final channels = [...controller.currentItems]; +/// This class can be mixed in or extended to create custom overrides. +class StreamChannelListEventHandler { + /// Function which gets called for the event + /// [EventType.channelDeleted]. + /// + /// This event is fired when a channel is deleted. + /// + /// By default, this removes the channel from the list of channels. + void onChannelDeleted(Event event, StreamChannelListController controller) { + final channels = [...controller.currentItems]; - final updatedChannels = channels - ..removeWhere( - (it) => it.cid == (event.cid ?? event.channel?.cid), - ); + final updatedChannels = channels + ..removeWhere( + (it) => it.cid == (event.cid ?? event.channel?.cid), + ); - controller.channels = updatedChannels; -} - -/// Handles [EventType.channelHidden] event. -/// -/// This event is fired when a channel is hidden. -/// -/// By default, this removes the channel from the list of channels. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelHidden: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelHidden( - Event event, - StreamChannelListController controller, -) { - onChannelDeleted(event, controller); -} - -/// Handles [EventType.channelTruncated] event. -/// -/// This event is fired when a channel is truncated. -/// -/// By default, this refreshes the whole channel list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelTruncated: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelTruncated( - Event event, - StreamChannelListController controller, -) { - controller.refresh(); -} - -/// Handles [EventType.channelUpdated] event. -/// -/// This event is fired when a channel is updated. -/// -/// By default, this updates the channel received in the event. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelUpdated: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelUpdated( - Event event, - StreamChannelListController controller, -) { - final eventChannel = event.channel; - if (eventChannel == null) return; - - final channels = [...controller.currentItems]; - final channelIndex = channels.indexWhere( - (it) => it.cid == (event.cid ?? eventChannel.cid), - ); - - if (channelIndex >= 0) { - final channelState = ChannelState(channel: eventChannel); - channels[channelIndex].state?.updateChannelState(channelState); + controller.channels = updatedChannels; } - controller.channels = channels; -} + /// Function which gets called for the event + /// [EventType.channelHidden]. + /// + /// This event is fired when a channel is hidden. + /// + /// By default, this removes the channel from the list of channels. + void onChannelHidden(Event event, StreamChannelListController controller) { + onChannelDeleted(event, controller); + } -/// Handles [EventType.channelVisible] event. -/// -/// This event is fired when a channel is made visible. -/// -/// By default, this adds the channel to the list of channels. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onChannelVisible: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onChannelVisible( - Event event, - StreamChannelListController controller, -) async { - final channelId = event.channelId; - final channelType = event.channelType; + /// Function which gets called for the event + /// [EventType.channelTruncated]. + /// + /// This event is fired when a channel is truncated. + /// + /// By default, this refreshes the whole channel list. + void onChannelTruncated(Event event, StreamChannelListController controller) { + controller.refresh(); + } - if (channelId == null || channelType == null) return; + /// Function which gets called for the event + /// [EventType.channelUpdated]. + /// + /// This event is fired when a channel is updated. + /// + /// By default, this updates the channel received in the event. + void onChannelUpdated(Event event, StreamChannelListController controller) { + final eventChannel = event.channel; + if (eventChannel == null) return; - final channel = await controller.getChannel( - id: channelId, - type: channelType, - ); - - final currentChannels = [...controller.currentItems]; - - final updatedChannels = [ - channel, - ...currentChannels..removeWhere((it) => it.cid == channel.cid), - ]; - - controller.channels = updatedChannels; -} - -/// Handles [EventType.connectionRecovered] event. -/// -/// This event is fired when the client web-socket connection recovers. -/// -/// By default, this refreshes the whole channel list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onConnectionRecovered: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onConnectionRecovered( - Event event, - StreamChannelListController controller, -) { - controller.refresh(); -} - -/// Handles [EventType.messageNew] event. -/// -/// This event is fired when a new message is created in one of the channels -/// we are currently watching. -/// -/// By default, this moves the channel to the top of the list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onMessageNew: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onMessageNew( - Event event, - StreamChannelListController controller, -) { - final channelCid = event.cid; - if (channelCid == null) return; - - final channels = [...controller.currentItems]; - - final channelIndex = channels.indexWhere((it) => it.cid == channelCid); - if (channelIndex <= 0) return; - - final channel = channels.removeAt(channelIndex); - channels.insert(0, channel); - - controller.channels = [...channels]; -} - -/// Handles [EventType.notificationAddedToChannel] event. -/// -/// This event is fired when a channel is added which we are not watching. -/// -/// By default, this adds the channel and moves it to the top of list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onNotificationAddedToChannel: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onNotificationAddedToChannel( - Event event, - StreamChannelListController controller, -) { - onChannelVisible(event, controller); -} - -/// Handles [EventType.notificationMessageNew] event. -/// -/// This event is fired when a new message is created in a channel which we are -/// not currently watching. -/// -/// By default, this adds the channel and moves it to the top of list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onNotificationMessageNew: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onNotificationMessageNew( - Event event, - StreamChannelListController controller, -) { - onChannelVisible(event, controller); -} - -/// Handles [EventType.notificationRemovedFromChannel] event. -/// -/// This event is fired when a user is removed from a channel which we are -/// not currently watching. -/// -/// By default, this removes the event channel from the list. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onNotificationRemovedFromChannel: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onNotificationRemovedFromChannel( - Event event, - StreamChannelListController controller, -) { - final channels = [...controller.currentItems]; - final updatedChannels = channels.where((it) => it.cid != event.channel?.cid); - final listChanged = channels.length != updatedChannels.length; - - if (!listChanged) return; - - controller.channels = [...updatedChannels]; -} - -/// Handles 'user.presence.changed' and [EventType.userUpdated] event. -/// -/// This event is fired when a user's presence changes or gets updated. -/// -/// By default, this updates the channel member with the event user. -/// -/// ```dart -/// StreamChannelListController( -/// client: client, -/// onUserPresenceChanged: (event, controller) { -/// // Do something -/// }, -/// ); -/// ``` -void onUserPresenceChanged( - Event event, - StreamChannelListController controller, -) { - final user = event.user; - if (user == null) return; - - final channels = [...controller.currentItems]; - - final updatedChannels = channels.map((channel) { - final members = [...channel.state!.members]; - final memberIndex = members.indexWhere( - (it) => user.id == (it.userId ?? it.user?.id), + final channels = [...controller.currentItems]; + final channelIndex = channels.indexWhere( + (it) => it.cid == (event.cid ?? eventChannel.cid), ); - if (memberIndex < 0) return channel; + if (channelIndex >= 0) { + final channelState = ChannelState(channel: eventChannel); + channels[channelIndex].state?.updateChannelState(channelState); + } - members[memberIndex] = members[memberIndex].copyWith(user: user); - final updatedState = ChannelState(members: [...members]); - channel.state!.updateChannelState(updatedState); + controller.channels = channels; + } - return channel; - }); + /// Function which gets called for the event + /// [EventType.channelVisible]. + /// + /// This event is fired when a channel is made visible. + /// + /// By default, this adds the channel to the list of channels. + void onChannelVisible( + Event event, + StreamChannelListController controller, + ) async { + final channelId = event.channelId; + final channelType = event.channelType; - controller.channels = [...updatedChannels]; + if (channelId == null || channelType == null) return; + + final channel = await controller.getChannel( + id: channelId, + type: channelType, + ); + + final currentChannels = [...controller.currentItems]; + + final updatedChannels = [ + channel, + ...currentChannels..removeWhere((it) => it.cid == channel.cid), + ]; + + controller.channels = updatedChannels; + } + + /// Function which gets called for the event + /// [EventType.connectionRecovered]. + /// + /// This event is fired when the client web-socket connection recovers. + /// + /// By default, this refreshes the whole channel list. + void onConnectionRecovered( + Event event, + StreamChannelListController controller, + ) { + controller.refresh(); + } + + /// Function which gets called for the event [EventType.messageNew]. + /// + /// This event is fired when a new message is created in one of the channels + /// we are currently watching. + /// + /// By default, this moves the channel to the top of the list. + void onMessageNew(Event event, StreamChannelListController controller) { + final channelCid = event.cid; + if (channelCid == null) return; + + final channels = [...controller.currentItems]; + + final channelIndex = channels.indexWhere((it) => it.cid == channelCid); + if (channelIndex <= 0) return; + + final channel = channels.removeAt(channelIndex); + channels.insert(0, channel); + + controller.channels = [...channels]; + } + + /// Function which gets called for the event + /// [EventType.notificationAddedToChannel]. + /// + /// This event is fired when a channel is added which we are not watching. + /// + /// By default, this adds the channel and moves it to the top of list. + void onNotificationAddedToChannel( + Event event, + StreamChannelListController controller, + ) { + onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationMessageNew]. + /// + /// This event is fired when a new message is created in a channel which we are + /// not currently watching. + /// + /// By default, this adds the channel and moves it to the top of list. + void onNotificationMessageNew( + Event event, + StreamChannelListController controller, + ) { + onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationRemovedFromChannel]. + /// + /// This event is fired when a user is removed from a channel which we are + /// not currently watching. + /// + /// By default, this removes the event channel from the list. + void onNotificationRemovedFromChannel( + Event event, + StreamChannelListController controller, + ) { + final channels = [...controller.currentItems]; + final updatedChannels = + channels.where((it) => it.cid != event.channel?.cid); + final listChanged = channels.length != updatedChannels.length; + + if (!listChanged) return; + + controller.channels = [...updatedChannels]; + } + + /// Function which gets called for the event + /// 'user.presence.changed' and [EventType.userUpdated]. + /// + /// This event is fired when a user's presence changes or gets updated. + /// + /// By default, this updates the channel member with the event user. + void onUserPresenceChanged( + Event event, + StreamChannelListController controller, + ) { + final user = event.user; + if (user == null) return; + + final channels = [...controller.currentItems]; + + final updatedChannels = channels.map((channel) { + final members = [...channel.state!.members]; + final memberIndex = members.indexWhere( + (it) => user.id == (it.userId ?? it.user?.id), + ); + + if (memberIndex < 0) return channel; + + members[memberIndex] = members[memberIndex].copyWith(user: user); + final updatedState = ChannelState(members: [...members]); + channel.state!.updateChannelState(updatedState); + + return channel; + }); + + controller.channels = [...updatedChannels]; + } } diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 76e3bc0e..a9d1416b 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -47,7 +47,7 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; -export 'src/v4/channel_list_view/channel_event_handlers.dart'; export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; +export 'src/v4/channel_list_view/stream_channel_list_event_handler.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; export 'src/visible_footnote.dart'; From f3110e99ebfbf4ebc96cb6c0a994e126736f5269 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 6 Dec 2021 17:28:40 +0530 Subject: [PATCH 033/112] refactor(ui): add channel list empty and error state widgets. Signed-off-by: xsahil03x --- .../stream_channel_list_event_handler.dart | 2 +- .../stream_channel_list_view.dart | 106 +++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart index bd6cf50d..449dd982 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart @@ -1,4 +1,4 @@ -import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat/stream_chat.dart' show ChannelState, Event; import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; /// Contains handlers that are called from [StreamChannelListController] for diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index af493815..67d4ad56 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -250,7 +250,12 @@ class _StreamChannelListViewState extends State { builder: (context, value, _) => value.when( (channels, nextPageKey, error) { if (channels.isEmpty) { - return const Center(child: Text('No channels')); + return const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: StreamChannelListEmpty(), + ), + ); } return ListView.separated( @@ -321,7 +326,11 @@ class _StreamChannelListViewState extends State { separatorBuilder: widget.separatorBuilder, itemBuilder: (_, __) => const StreamChannelListLoadingTile(), ), - error: (error) => Center(child: Text('Error: $error')), + error: (error) => Center( + child: StreamChannelListError( + onPressed: _controller.refresh, + ), + ), ), ); } @@ -347,11 +356,11 @@ class ChannelListLoadMoreError extends StatelessWidget { /// Creates a new instance of [ChannelListLoadMoreError]. const ChannelListLoadMoreError({ Key? key, - required this.onTap, + this.onTap, }) : super(key: key); /// The callback to invoke when the user taps on the error indicator. - final GestureTapCallback onTap; + final GestureTapCallback? onTap; @override Widget build(BuildContext context) { @@ -395,3 +404,92 @@ class StreamChannelListSeparator extends StatelessWidget { ); } } + +/// A widget that is used to display an error screen +/// when [StreamChannelListController] fails to load initial channels. +class StreamChannelListError extends StatelessWidget { + /// Creates a new instance of [StreamChannelListError] widget. + const StreamChannelListError({ + Key? key, + this.onPressed, + }) : super(key: key); + + /// The callback to invoke when the user taps on the retry button. + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(right: 2), + child: Icon(Icons.error_outline), + ), + ), + TextSpan(text: context.translations.loadingChannelsError), + ], + ), + style: Theme.of(context).textTheme.headline6, + ), + TextButton( + onPressed: onPressed, + child: Text(context.translations.retryLabel), + ), + ], + ); +} + +/// A widget that is used to display an empty state when +/// [StreamChannelListController] loads zero channels. +class StreamChannelListEmpty extends StatelessWidget { + /// Creates a new instance of [StreamChannelListEmpty] widget. + const StreamChannelListEmpty({ + Key? key, + this.onPressed, + }) : super(key: key); + + /// The callback to invoke when the user taps on the start a chat button. + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + StreamSvgIcon.message( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + const SizedBox(height: 28), + Text( + context.translations.letsStartChattingLabel, + style: chatThemeData.textTheme.headline, + ), + const SizedBox(height: 8), + Text( + context.translations.sendingFirstMessageLabel, + textAlign: TextAlign.center, + style: chatThemeData.textTheme.body.copyWith( + color: chatThemeData.colorTheme.textLowEmphasis, + ), + ), + const Spacer(), + TextButton( + onPressed: onPressed, + child: Text( + context.translations.startAChatLabel, + style: chatThemeData.textTheme.bodyBold.copyWith( + color: chatThemeData.colorTheme.accentPrimary, + ), + ), + ), + ], + ); + } +} From 093a66854641f331ad00eee68fb48cb10092a214 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Dec 2021 17:48:57 +0530 Subject: [PATCH 034/112] refactor(ui): add stream_channel_info_bottom_sheet.dart Signed-off-by: xsahil03x --- .../lib/src/paged_value_notifier.dart | 6 +- .../v4/stream_channel_info_bottom_sheet.dart | 365 ++++++++++++++++++ .../lib/stream_chat_flutter.dart | 3 + 3 files changed, 372 insertions(+), 2 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart index d2f5b373..5ef37374 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -62,10 +62,12 @@ abstract class PagedValueNotifier /// Refresh the data presented by this [PagedValueNotifier]. /// + /// Resets the [value] to the initial value in case [resetValue] is true. + /// /// Note: This API is intended for UI-driven refresh signals, /// such as swipe-to-refresh. - Future refresh() { - value = _initialValue; + Future refresh({bool resetValue = true}) { + if (resetValue) value = _initialValue; return doInitialLoad(); } diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart new file mode 100644 index 00000000..51bda67d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart @@ -0,0 +1,365 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/channel_info.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/option_list_tile.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/theme/themes.dart'; +import 'package:stream_chat_flutter/src/user_avatar.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; + +/// A [BottomSheet] that shows information about a [Channel]. +class StreamChannelInfoBottomSheet extends StatelessWidget { + /// Creates a new instance [StreamChannelInfoBottomSheet] widget. + StreamChannelInfoBottomSheet({ + Key? key, + required this.channel, + this.onMemberTap, + this.onViewInfoTap, + this.onLeaveChannelTap, + this.onDeleteConversationTap, + this.onCancelTap, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The [Channel] to show information about. + final Channel channel; + + /// A callback that is called when a member is tapped. + final void Function(Member)? onMemberTap; + + /// A callback that is called when the "View Info" button is tapped. + final VoidCallback? onViewInfoTap; + + /// A callback that is called when the "Leave Channel" button is tapped. + /// + /// Only shown when the channel is a group channel. + final VoidCallback? onLeaveChannelTap; + + /// A callback that is called when the "Delete Conversation" button is tapped. + /// + /// Only shown when you are the `owner` of the channel. + final VoidCallback? onDeleteConversationTap; + + /// A callback that is called when the "Cancel" button is tapped. + final VoidCallback? onCancelTap; + + @override + Widget build(BuildContext context) { + final themeData = StreamChatTheme.of(context); + final colorTheme = themeData.colorTheme; + final channelPreviewTheme = ChannelPreviewTheme.of(context); + + final currentUser = channel.client.state.currentUser; + final isOneToOneChannel = channel.isDistinct && channel.memberCount == 2; + + final members = channel.state?.members ?? []; + + final isOwner = members.any( + (it) => it.user?.id == currentUser?.id && it.role == 'owner', + ); + + // remove current user in case it's 1-1 conversation + if (isOneToOneChannel) { + members.removeWhere((it) => it.user?.id == currentUser?.id); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 24), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamChannelName( + channel: channel, + textStyle: themeData.textTheme.headlineBold, + ), + ), + ), + const SizedBox(height: 5), + Center( + // TODO: Refactor ChannelInfo + child: ChannelInfo( + showTypingIndicator: false, + channel: channel, + textStyle: channelPreviewTheme.subtitleStyle, + ), + ), + const SizedBox(height: 17), + Container( + height: 94, + alignment: Alignment.center, + child: ListView.separated( + shrinkWrap: true, + itemCount: members.length, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + separatorBuilder: (context, index) => const SizedBox(width: 16), + itemBuilder: (context, index) { + final member = members[index]; + final user = member.user!; + return Column( + children: [ + UserAvatar( + user: user, + constraints: const BoxConstraints( + maxHeight: 64, + maxWidth: 64, + ), + borderRadius: BorderRadius.circular(32), + onlineIndicatorConstraints: BoxConstraints.tight( + const Size(12, 12), + ), + onTap: onMemberTap != null + ? (_) => onMemberTap!(member) + : null, + ), + const SizedBox(height: 6), + Text( + user.name, + style: themeData.textTheme.footnoteBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ); + }, + ), + ), + const SizedBox(height: 24), + OptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.user( + color: colorTheme.textLowEmphasis, + ), + ), + title: context.translations.viewInfoLabel, + onTap: onViewInfoTap, + ), + if (!isOneToOneChannel) + OptionListTile( + title: context.translations.leaveGroupLabel, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.userRemove( + color: colorTheme.textLowEmphasis, + ), + ), + onTap: onLeaveChannelTap, + ), + if (isOwner) + OptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.delete( + color: colorTheme.accentError, + ), + ), + title: context.translations.deleteConversationLabel, + titleColor: colorTheme.accentError, + onTap: onDeleteConversationTap, + ), + OptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.closeSmall( + color: colorTheme.textLowEmphasis, + ), + ), + title: context.translations.cancelLabel, + onTap: onCancelTap ?? Navigator.of(context).pop, + ), + ], + ); + } +} + +const _kDefaultChannelInfoBottomSheetShape = RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), +); + +/// Shows a modal material design bottom sheet. +/// +/// A modal bottom sheet is an alternative to a menu or a dialog and prevents +/// the user from interacting with the rest of the app. +/// +/// A closely related widget is a persistent bottom sheet, which shows +/// information that supplements the primary content of the app without +/// preventing the use from interacting with the app. Persistent bottom sheets +/// can be created and displayed with the [showBottomSheet] function or the +/// [ScaffoldState.showBottomSheet] method. +/// +/// The `context` argument is used to look up the [Navigator] and [Theme] for +/// the bottom sheet. It is only used when the method is called. Its +/// corresponding widget can be safely removed from the tree before the bottom +/// sheet is closed. +/// +/// The `isScrollControlled` parameter specifies whether this is a route for +/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish +/// to have a bottom sheet that has a scrollable child such as a [ListView] or +/// a [GridView] and have the bottom sheet be draggable, you should set this +/// parameter to true. +/// +/// The `useRootNavigator` parameter ensures that the root navigator is used to +/// display the [BottomSheet] when set to `true`. This is useful in the case +/// that a modal [BottomSheet] needs to be displayed above all other content +/// but the caller is inside another [Navigator]. +/// +/// The [isDismissible] parameter specifies whether the bottom sheet will be +/// dismissed when user taps on the scrim. +/// +/// The [enableDrag] parameter specifies whether the bottom sheet can be +/// dragged up and down and dismissed by swiping downwards. +/// +/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], +/// [constraints] and [transitionAnimationController] +/// parameters can be passed in to customize the appearance and behavior of +/// modal bottom sheets (see the documentation for these on [BottomSheet] +/// for more details). +/// +/// The [transitionAnimationController] controls the bottom sheet's entrance and +/// exit animations if provided. +/// +/// The optional `routeSettings` parameter sets the [RouteSettings] of the modal bottom sheet +/// sheet. This is particularly useful in the case that a user wants to observe +/// [PopupRoute]s within a [NavigatorObserver]. +/// +/// Returns a `Future` that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the modal bottom sheet was closed. +/// +/// See also: +/// +/// * [BottomSheet], which becomes the parent of the widget returned by the +/// function passed as the `builder` argument to [showModalBottomSheet]. +/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing +/// non-modal bottom sheets. +/// * [DraggableScrollableSheet], which allows you to create a bottom sheet +/// that grows and then becomes scrollable once it reaches its maximum size. +/// * +Future showChannelInfoModalBottomSheet({ + required BuildContext context, + required Channel channel, + Color? backgroundColor, + double? elevation, + BoxConstraints? constraints, + Color? barrierColor, + bool isScrollControlled = true, + bool useRootNavigator = false, + bool isDismissible = true, + bool enableDrag = true, + RouteSettings? routeSettings, + AnimationController? transitionAnimationController, + Clip? clipBehavior = Clip.hardEdge, + ShapeBorder? shape = _kDefaultChannelInfoBottomSheetShape, + void Function(Member)? onMemberTap, + VoidCallback? onViewInfoTap, + VoidCallback? onLeaveChannelTap, + VoidCallback? onDeleteConversationTap, + VoidCallback? onCancelTap, +}) => + showModalBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + barrierColor: barrierColor, + isScrollControlled: isScrollControlled, + useRootNavigator: useRootNavigator, + isDismissible: isDismissible, + enableDrag: enableDrag, + routeSettings: routeSettings, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), + ); + +/// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If +/// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet]. +/// +/// Returns a controller that can be used to close and otherwise manipulate the +/// bottom sheet. +/// +/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], +/// [constraints] and [transitionAnimationController] +/// parameters can be passed in to customize the appearance and behavior of +/// persistent bottom sheets (see the documentation for these on [BottomSheet] +/// for more details). +/// +/// To rebuild the bottom sheet (e.g. if it is stateful), call +/// [PersistentBottomSheetController.setState] on the controller returned by +/// this method. +/// +/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing +/// [ModalRoute] and a back button is added to the app bar of the [Scaffold] +/// that closes the bottom sheet. +/// +/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and +/// does not add a back button to the enclosing Scaffold's app bar, use the +/// [Scaffold.bottomSheet] constructor parameter. +/// +/// A closely related widget is a modal bottom sheet, which is an alternative +/// to a menu or a dialog and prevents the user from interacting with the rest +/// of the app. Modal bottom sheets can be created and displayed with the +/// [showModalBottomSheet] function. +/// +/// The `context` argument is used to look up the [Scaffold] for the bottom +/// sheet. It is only used when the method is called. Its corresponding widget +/// can be safely removed from the tree before the bottom sheet is closed. +/// +/// See also: +/// +/// * [BottomSheet], which becomes the parent of the widget returned by the +/// `builder`. +/// * [showModalBottomSheet], which can be used to display a modal bottom +/// sheet. +/// * [Scaffold.of], for information about how to obtain the [BuildContext]. +/// * +PersistentBottomSheetController showChannelInfoBottomSheet({ + required BuildContext context, + required Channel channel, + Color? backgroundColor, + double? elevation, + BoxConstraints? constraints, + AnimationController? transitionAnimationController, + Clip? clipBehavior = Clip.hardEdge, + ShapeBorder? shape = _kDefaultChannelInfoBottomSheetShape, + void Function(Member)? onMemberTap, + VoidCallback? onViewInfoTap, + VoidCallback? onLeaveChannelTap, + VoidCallback? onDeleteConversationTap, + VoidCallback? onCancelTap, +}) => + showBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), + ); diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index a9d1416b..749a1d63 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -47,7 +47,10 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; + +// v4 export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; export 'src/v4/channel_list_view/stream_channel_list_event_handler.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/v4/stream_channel_info_bottom_sheet.dart'; export 'src/visible_footnote.dart'; From c301b51c8381e59e34f78656d1d6fb1df0c86d4e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Dec 2021 17:49:50 +0530 Subject: [PATCH 035/112] refactor(ui): improve channel list and controller Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 8 ++ .../stream_channel_list_controller.dart | 29 ++++ .../stream_channel_list_tile.dart | 84 ++++++++--- .../stream_channel_list_view.dart | 136 ++++++++++-------- 4 files changed, 178 insertions(+), 79 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 51034d3a..e2660099 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1833,6 +1833,14 @@ class ChannelClientState { (watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), ); + /// Channel member for the current user. + Member? get currentUserMember => members.firstWhereOrNull( + (m) => m.user?.id == _channel.client.state.currentUser?.id, + ); + + /// User role for the current user. + String? get currentUserRole => currentUserMember?.role; + /// Channel read list. List get read => _channelState.read; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 5dcbeaa2..c94ac616 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -176,6 +176,32 @@ class StreamChannelListController extends PagedValueNotifier { return channel; } + /// Leaves the [channel] and updates the list. + Future leaveChannel(Channel channel) async { + final user = client.state.currentUser; + assert(user != null, 'You must be logged in to leave a channel.'); + await channel.removeMembers([user!.id]); + } + + /// Deletes the [channel] and updates the list. + Future deleteChannel(Channel channel) async { + await channel.delete(); + } + + /// Mutes the [channel] and updates the list. + Future muteChannel(Channel channel) async { + await channel.mute(); + } + + /// Un-mutes the [channel] and updates the list. + Future unmuteChannel(Channel channel) async { + await channel.unmute(); + } + + /// Event listener, which can be set in order to listen + /// [client] web-socket events. + bool Function(Event event)? eventListener; + StreamSubscription? _channelEventSubscription; // Subscribes to the channel list events. @@ -185,6 +211,9 @@ class StreamChannelListController extends PagedValueNotifier { } _channelEventSubscription = client.on().listen((event) { + // Returns early if the event is already handled by the listener. + if (eventListener?.call(event) ?? false) return; + final eventType = event.type; if (eventType == EventType.channelDeleted) { _eventHandler.onChannelDeleted(event, this); diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index a3d0f67a..d58e1441 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -31,10 +31,14 @@ class StreamChannelListTile extends StatelessWidget { this.leading, this.title, this.subtitle, + this.trailing, this.onTap, this.onLongPress, + this.tileColor, this.visualDensity = VisualDensity.compact, this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + this.unreadIndicatorBuilder, + this.sendingIndicatorBuilder, }) : assert( channel.state != null, 'Channel ${channel.id} is not initialized', @@ -53,12 +57,23 @@ class StreamChannelListTile extends StatelessWidget { /// Additional content displayed below the title. final Widget? subtitle; + /// A widget to display at the end of tile. + final Widget? trailing; + /// Called when the user taps this list tile. final GestureTapCallback? onTap; /// Called when the user long-presses on this list tile. final GestureLongPressCallback? onLongPress; + /// {@template flutter.material.ListTile.tileColor} + /// Defines the background color of `ListTile` when [selected] is false. + /// + /// When the value is null, the `tileColor` is set to [ListTileTheme.tileColor] + /// if it's not null and to [Colors.transparent] if it's null. + /// {@endtemplate} + final Color? tileColor; + /// Defines how compact the list tile's layout will be. /// /// {@macro flutter.material.themedata.visualDensity} @@ -77,6 +92,40 @@ class StreamChannelListTile extends StatelessWidget { /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. final EdgeInsetsGeometry contentPadding; + /// The widget builder for the unread indicator. + final WidgetBuilder? unreadIndicatorBuilder; + + /// The widget builder for the sending indicator. + /// + /// `Message` is the last message in the channel, Use it to determine the + /// status using [Message.status]. + final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamChannelListTile copyWith({ + Key? key, + Channel? channel, + Widget? leading, + Widget? title, + Widget? subtitle, + VoidCallback? onTap, + VoidCallback? onLongPress, + VisualDensity? visualDensity, + EdgeInsetsGeometry? contentPadding, + }) => + StreamChannelListTile( + key: key ?? this.key, + channel: channel ?? this.channel, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + ); + @override Widget build(BuildContext context) { final channelState = channel.state!; @@ -101,6 +150,12 @@ class StreamChannelListTile extends StatelessWidget { textStyle: channelPreviewTheme.subtitleStyle, ); + final trailing = this.trailing ?? + ChannelLastMessageDate( + channel: channel, + textStyle: channelPreviewTheme.lastMessageAtStyle, + ); + return BetterStreamBuilder( stream: channel.isMutedStream, initialData: channel.isMuted, @@ -113,6 +168,7 @@ class StreamChannelListTile extends StatelessWidget { visualDensity: visualDensity, contentPadding: contentPadding, leading: leading, + tileColor: tileColor, title: Row( children: [ Expanded(child: title), @@ -125,7 +181,8 @@ class StreamChannelListTile extends StatelessWidget { !members.any((it) => it.user!.id == currentUser.id)) { return const Offstage(); } - return UnreadIndicator(cid: channel.cid); + return unreadIndicatorBuilder?.call(context) ?? + UnreadIndicator(cid: channel.cid); }, ), ], @@ -154,24 +211,19 @@ class StreamChannelListTile extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 4), - child: SendingIndicator( - message: lastMessage, - size: channelPreviewTheme.indicatorIconSize, - isMessageRead: channelState.read - .where((it) => it.user.id != currentUser.id) - .where( - (it) => it.lastRead.isAfter(lastMessage.createdAt), - ) - .isNotEmpty, - ), + child: + sendingIndicatorBuilder?.call(context, lastMessage) ?? + SendingIndicator( + message: lastMessage, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: channelState + .currentUserRead!.lastRead + .isAfter(lastMessage.createdAt), + ), ); }, ), - ChannelLastMessageDate( - channel: channel, - textStyle: channelPreviewTheme.lastMessageAtStyle, - ), - // trailing ?? _buildDate(context), + trailing, ], ), ), diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index 67d4ad56..1527493e 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -19,6 +19,7 @@ Widget defaultSeparatorBuilder(BuildContext context, int index) => typedef StreamChannelListViewItemBuilder = Widget Function( BuildContext context, Channel channel, + StreamChannelListTile defaultWidget, ); /// A [ListView] that shows a list of [Channel]s, @@ -51,6 +52,9 @@ class StreamChannelListView extends StatefulWidget { required this.controller, this.itemBuilder, this.separatorBuilder = defaultSeparatorBuilder, + this.emptyBuilder, + this.loadingBuilder, + this.errorBuilder, this.onChannelTap, this.onChannelLongPress, this.padding, @@ -71,13 +75,29 @@ class StreamChannelListView extends StatefulWidget { /// A builder that is called to build items in the [ListView]. /// - /// The `index` parameter is the index of the list tile in the list and the - /// `channel` parameter is the [Channel] at that position. + /// The `channel` parameter is the [Channel] at this position in the list + /// and the `defaultWidget` is the default widget used + /// i.e: [StreamChannelListTile]. final StreamChannelListViewItemBuilder? itemBuilder; /// A builder that is called to build the list separator. final IndexedWidgetBuilder separatorBuilder; + /// A builder that is called to build the empty state of the list. + /// + /// If not provider, [StreamChannelListEmptyWidget] will be used. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the loading state of the list. + /// + /// If not provided, [StreamChannelListLoadingTile] will be used. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the list. + /// + /// If not provided, [StreamChannelListErrorWidget] will be used. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + /// Called when the user taps this list tile. final void Function(Channel)? onChannelTap; @@ -250,12 +270,13 @@ class _StreamChannelListViewState extends State { builder: (context, value, _) => value.when( (channels, nextPageKey, error) { if (channels.isEmpty) { - return const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: StreamChannelListEmpty(), - ), - ); + return widget.emptyBuilder?.call(context) ?? + const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: StreamChannelListEmptyWidget(), + ), + ); } return ListView.separated( @@ -290,47 +311,61 @@ class _StreamChannelListViewState extends State { if (index == channels.length) { if (error != null) { - return ChannelListLoadMoreError( + return StreamChannelListLoadMoreError( onTap: _controller.retry, ); } return const Center( child: Padding( padding: EdgeInsets.all(16), - child: ChannelListLoadMoreIndicator(), + child: StreamChannelListLoadMoreIndicator(), ), ); } final channel = channels[index]; - final itemBuilder = widget.itemBuilder; - if (itemBuilder != null) return itemBuilder(context, channel); final onTap = widget.onChannelTap; final onLongPress = widget.onChannelLongPress; - return StreamChannelListTile( + final streamChannelListTile = StreamChannelListTile( channel: channel, onTap: onTap == null ? null : () => onTap(channel), onLongPress: onLongPress == null ? null : () => onLongPress(channel), ); + + final itemBuilder = widget.itemBuilder; + + if (itemBuilder != null) { + return itemBuilder( + context, + channel, + streamChannelListTile, + ); + } + + return streamChannelListTile; }, ); }, - loading: () => ListView.separated( - padding: widget.padding, - physics: widget.physics, - reverse: widget.reverse, - itemCount: 25, - separatorBuilder: widget.separatorBuilder, - itemBuilder: (_, __) => const StreamChannelListLoadingTile(), - ), - error: (error) => Center( - child: StreamChannelListError( - onPressed: _controller.refresh, - ), - ), + loading: () => + widget.loadingBuilder?.call(context) ?? + ListView.separated( + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + itemCount: 25, + separatorBuilder: widget.separatorBuilder, + itemBuilder: (_, __) => const StreamChannelListLoadingTile(), + ), + error: (error) => + widget.errorBuilder?.call(context, error) ?? + Center( + child: StreamChannelListErrorWidget( + onPressed: _controller.refresh, + ), + ), ), ); } @@ -338,9 +373,9 @@ class _StreamChannelListViewState extends State { /// A [StreamChannelListTile] that can be used in a [ListView] to show a /// loading tile while waiting for the [StreamChannelListController] to load /// more channels. -class ChannelListLoadMoreIndicator extends StatelessWidget { - /// Creates a new instance of [ChannelListLoadMoreIndicator]. - const ChannelListLoadMoreIndicator({Key? key}) : super(key: key); +class StreamChannelListLoadMoreIndicator extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadMoreIndicator]. + const StreamChannelListLoadMoreIndicator({Key? key}) : super(key: key); @override Widget build(BuildContext context) => const SizedBox( @@ -352,9 +387,9 @@ class ChannelListLoadMoreIndicator extends StatelessWidget { /// A [StreamChannelListTile] that is used to display the error indicator when /// loading more channels fails. -class ChannelListLoadMoreError extends StatelessWidget { - /// Creates a new instance of [ChannelListLoadMoreError]. - const ChannelListLoadMoreError({ +class StreamChannelListLoadMoreError extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadMoreError]. + const StreamChannelListLoadMoreError({ Key? key, this.onTap, }) : super(key: key); @@ -407,9 +442,9 @@ class StreamChannelListSeparator extends StatelessWidget { /// A widget that is used to display an error screen /// when [StreamChannelListController] fails to load initial channels. -class StreamChannelListError extends StatelessWidget { - /// Creates a new instance of [StreamChannelListError] widget. - const StreamChannelListError({ +class StreamChannelListErrorWidget extends StatelessWidget { + /// Creates a new instance of [StreamChannelListErrorWidget] widget. + const StreamChannelListErrorWidget({ Key? key, this.onPressed, }) : super(key: key); @@ -445,15 +480,9 @@ class StreamChannelListError extends StatelessWidget { /// A widget that is used to display an empty state when /// [StreamChannelListController] loads zero channels. -class StreamChannelListEmpty extends StatelessWidget { - /// Creates a new instance of [StreamChannelListEmpty] widget. - const StreamChannelListEmpty({ - Key? key, - this.onPressed, - }) : super(key: key); - - /// The callback to invoke when the user taps on the start a chat button. - final VoidCallback? onPressed; +class StreamChannelListEmptyWidget extends StatelessWidget { + /// Creates a new instance of [StreamChannelListEmptyWidget] widget. + const StreamChannelListEmptyWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -461,7 +490,6 @@ class StreamChannelListEmpty extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Spacer(), StreamSvgIcon.message( size: 148, color: chatThemeData.colorTheme.disabled, @@ -471,24 +499,6 @@ class StreamChannelListEmpty extends StatelessWidget { context.translations.letsStartChattingLabel, style: chatThemeData.textTheme.headline, ), - const SizedBox(height: 8), - Text( - context.translations.sendingFirstMessageLabel, - textAlign: TextAlign.center, - style: chatThemeData.textTheme.body.copyWith( - color: chatThemeData.colorTheme.textLowEmphasis, - ), - ), - const Spacer(), - TextButton( - onPressed: onPressed, - child: Text( - context.translations.startAChatLabel, - style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentPrimary, - ), - ), - ), ], ); } From 51e7c873cb1cd89fc7193ac9bda000da311ac22a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 8 Dec 2021 17:55:55 +0530 Subject: [PATCH 036/112] refactor(ui): minor doc changes Signed-off-by: xsahil03x --- .../v4/channel_list_view/stream_channel_list_controller.dart | 3 +++ .../lib/src/v4/channel_list_view/stream_channel_list_tile.dart | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index c94ac616..488452bd 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -200,6 +200,9 @@ class StreamChannelListController extends PagedValueNotifier { /// Event listener, which can be set in order to listen /// [client] web-socket events. + /// + /// Return `true` if the event is handled. Return `false` to + /// allow the event to be handled internally. bool Function(Event event)? eventListener; StreamSubscription? _channelEventSubscription; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index d58e1441..685c556c 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -67,7 +67,7 @@ class StreamChannelListTile extends StatelessWidget { final GestureLongPressCallback? onLongPress; /// {@template flutter.material.ListTile.tileColor} - /// Defines the background color of `ListTile` when [selected] is false. + /// Defines the background color of `ListTile`. /// /// When the value is null, the `tileColor` is set to [ListTileTheme.tileColor] /// if it's not null and to [Colors.transparent] if it's null. From ff2c8801d5a515220154f89e35b1236bec15aeaf Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 10 Dec 2021 10:00:30 +0100 Subject: [PATCH 037/112] 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 038/112] 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 039/112] 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 040/112] 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 527ac4563f10ad9c2e9de8f5dee6d94699703903 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 13 Dec 2021 11:33:46 +0100 Subject: [PATCH 041/112] 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 042/112] 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 043/112] 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 044/112] 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 045/112] 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 046/112] 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 047/112] 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 099e9046143bd3f5aa817d9d77b80ba783ad3a1e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 17 Dec 2021 21:50:34 +0530 Subject: [PATCH 048/112] 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 e19cc87362cceb06c1ac5dd72cf25c824085d7e3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 20 Dec 2021 16:11:12 +0530 Subject: [PATCH 049/112] 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 050/112] 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 051/112] 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 052/112] 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 053/112] 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 054/112] 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 055/112] 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 056/112] 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 057/112] 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 058/112] 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 059/112] 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 060/112] 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 061/112] 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 062/112] 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 063/112] 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 5d163b9a860f90fa3d740483a1baabebcf94f4fd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 5 Jan 2022 11:18:46 +0100 Subject: [PATCH 064/112] add missing permissions and ui --- .../stream_chat/lib/src/client/channel.dart | 12 + .../stream_chat/lib/src/permission_type.dart | 12 + .../lib/src/channel_info.dart | 3 +- .../lib/src/channel_list_view.dart | 10 +- .../lib/src/localization/translations.dart | 7 + .../lib/src/message_actions_modal.dart | 13 +- .../lib/src/message_input/message_input.dart | 220 ++++++++++-------- .../lib/src/message_list_view.dart | 4 +- .../lib/src/message_reactions_modal.dart | 8 +- .../lib/src/message_widget.dart | 7 + 10 files changed, 171 insertions(+), 125 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 258122a7..db39628b 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -300,6 +300,18 @@ class Channel { return data; } + /// List of user permissions on this channel + List get ownCapabilities => + state?._channelState.channel?.ownCapabilities ?? []; + + /// List of user permissions on this channel + Stream> get ownCapabilitiesStream { + _checkInitialized(); + return state!.channelStateStream + .map((cs) => cs.channel?.ownCapabilities ?? []) + .distinct(); + } + /// Channel extra data as a stream. Stream> get extraDataStream { _checkInitialized(); diff --git a/packages/stream_chat/lib/src/permission_type.dart b/packages/stream_chat/lib/src/permission_type.dart index b0e2e73f..931f2848 100644 --- a/packages/stream_chat/lib/src/permission_type.dart +++ b/packages/stream_chat/lib/src/permission_type.dart @@ -5,6 +5,9 @@ class PermissionType { /// and user has CreateMessage permission. static const String sendMessage = 'send-message'; + /// Capability required to receive connect events in the channel + static const String connectEvents = 'connect-events'; + /// Capability required to send a message /// Reactions are enabled for the channel, channel is not frozen /// (or user has UseFrozenChannel permission) and user has @@ -32,10 +35,19 @@ class PermissionType { /// User has RemoveOwnChannelMembership or UpdateChannelMembers permission static const String leaveChannel = 'leave-channel'; + /// Ability to receive read events + static const String readEvents = 'read-events'; + /// Capability required to pin a message in a channel /// Corresponds to PinMessage permission static const String pinMessage = 'pin-message'; + /// Capability required to quote a message in a channel + static const String quoteMessage = 'quote-message'; + + /// Capability required to flag a message in a channel + static const String flagMessage = 'flag-message'; + /// User has ability to delete any message in the channel /// User has DeleteMessage permission /// which applies to any message in the channel diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 62bfd1de..791805af 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -61,7 +61,8 @@ class ChannelInfo extends StatelessWidget { var text = context.translations.membersCountText(memberCount); final onlineCount = members?.where((m) => m.user?.online == true).length ?? 0; - if (onlineCount > 0) { + if (channel.ownCapabilities.contains(PermissionType.connectEvents) && + onlineCount > 0) { text += ', ${context.translations.watchersCountText(onlineCount)}'; } alternativeWidget = Text( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index fd59489a..bf59991e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -560,14 +560,8 @@ class _ChannelListViewState extends State { ); }, ), - if ([ - 'admin', - 'owner', - ].contains(channel.state!.members - .firstWhereOrNull( - (m) => m.userId == channel.client.state.currentUser?.id, - ) - ?.role)) + if (channel.ownCapabilities + .contains(PermissionType.deleteChannel)) IconSlideAction( color: backgroundColor, iconWidget: StreamSvgIcon.delete( diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 8039ac47..5dfaf60b 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -93,6 +93,9 @@ abstract class Translations { /// The label for search Gif String get searchGifLabel; + /// The label for the MessageInput hint when permission denied on sendMessage + String get sendMessagePermissionError; + /// The label for add a comment or send in case of /// attachments inside [MessageInput] String get addACommentOrSendLabel; @@ -377,6 +380,10 @@ class DefaultTranslations implements Translations { return 'Pinned by ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'You don\'t have permission to send messages'; + @override String get emptyMessagesText => 'There are no messages currently'; 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 115069d9..fa5c3c04 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -191,7 +191,9 @@ class _MessageActionsModalState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (widget.showReplyMessage && + if (_userPermissions + .contains(PermissionType.quoteMessage) && + widget.showReplyMessage && widget.message.status == MessageSendingStatus.sent) _buildReplyButton(context), if ((widget.showThreadReplyMessage ?? @@ -207,7 +209,10 @@ class _MessageActionsModalState extends State { _isMyMessage && hasEditPermission) _buildEditMessage(context), if (widget.showCopyMessage) _buildCopyButton(context), - if (widget.showFlagButton) _buildFlagButton(context), + if (_userPermissions + .contains(PermissionType.flagMessage) && + widget.showFlagButton) + _buildFlagButton(context), if (widget.showPinButton ?? _userPermissions .contains(PermissionType.pinMessage)) @@ -680,9 +685,7 @@ class _MessageActionsModalState extends State { @override void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); - _userPermissions = - newStreamChannel.channel.state?.channelState.channel?.ownCapabilities ?? - []; + _userPermissions = newStreamChannel.channel.ownCapabilities; _isMyMessage = widget.message.user!.id == newStreamChannel.channel.client.state.currentUser!.id; super.didChangeDependencies(); 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..6e4102af 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 @@ -468,112 +468,127 @@ class MessageInputState extends State void _stopSlowMode() => _slowModeTimer?.cancel(); @override - Widget build(BuildContext context) => MessageValueListenableBuilder( - valueListenable: _effectiveController, - builder: (context, value, _) { - 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 build(BuildContext context) { + if (!StreamChannel.of(context) + .channel + .ownCapabilities + .contains(PermissionType.sendMessage)) { + return SizedBox( + height: 50, + child: FittedBox( + child: Text( + context.translations.sendMessagePermissionError, + style: _messageInputTheme.inputTextStyle, + ), + ), + ); + } + return MessageValueListenableBuilder( + valueListenable: _effectiveController, + builder: (context, value, _) { + 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; + }); } - }, - 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: () { - _effectiveController.clearQuotedMessage(); - _focusNode.unfocus(); - }, - ), - ], - ), - ), + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_hasQuotedMessage) 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(), + 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: () { + _effectiveController.clearQuotedMessage(); + _focusNode.unfocus(); + }, + ), + ], ), - _buildFilePickerSection(), - ], - ), + ), + 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, - ], + ), + ); + 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, + ); + }, + ); + } Flex _buildTextField(BuildContext context) => Flex( direction: Axis.horizontal, @@ -701,7 +716,9 @@ class MessageInputState extends State ? const Offstage() : Wrap( children: [ - if (!widget.disableAttachments) + if (!widget.disableAttachments && + channel.ownCapabilities + .contains(PermissionType.uploadFile)) _buildAttachmentButton(context), if (widget.showCommandsButton && !_isEditing && @@ -881,7 +898,8 @@ class MessageInputState extends State value = value.trim(); final channel = StreamChannel.of(context).channel; - if (value.isNotEmpty) { + if (channel.ownCapabilities.contains(PermissionType.sendTypingEvents) && + value.isNotEmpty) { channel .keyStroke(_effectiveController.value.parentId) // ignore: no-empty-block diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 4531b797..f70acd52 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1296,9 +1296,7 @@ class _MessageListViewState extends State { void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); _streamTheme = StreamChatTheme.of(context); - _userPermissions = - newStreamChannel.channel.state?.channelState.channel?.ownCapabilities ?? - []; + _userPermissions = newStreamChannel.channel.ownCapabilities; if (newStreamChannel != streamChannel) { streamChannel = newStreamChannel; diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 4b85412c..37c8afff 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -45,13 +45,7 @@ class MessageReactionsModal extends StatelessWidget { Widget build(BuildContext context) { final size = MediaQuery.of(context).size; final user = StreamChat.of(context).currentUser; - final _userPermissions = StreamChannel.of(context) - .channel - .state - ?.channelState - .channel - ?.ownCapabilities ?? - []; + final _userPermissions = StreamChannel.of(context).channel.ownCapabilities; final hasReactionPermission = _userPermissions.contains(PermissionType.sendReaction); diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 60bacc29..d48c5099 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1250,6 +1250,13 @@ class _MessageWidgetState extends State final channel = StreamChannel.of(context).channel; + if (!channel.ownCapabilities.contains(PermissionType.readEvents)) { + return SendingIndicator( + message: message, + size: style!.fontSize, + ); + } + return BetterStreamBuilder>( stream: channel.state?.readStream, initialData: channel.state?.read, From abac011876ca75adebf99a259a523b9928f26ac9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 5 Jan 2022 11:41:31 +0100 Subject: [PATCH 065/112] add missing permissions and ui --- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../lib/src/message_input/message_input.dart | 9 ++++++--- .../stream_chat_flutter/lib/src/message_list_view.dart | 5 ++++- packages/stream_chat_flutter/lib/src/message_widget.dart | 9 ++++++--- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 1721e6f1..4b1a8d5d 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..3db53b6e 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ .channel .ownCapabilities .contains(PermissionType.sendMessage)) { - return SizedBox( - height: 50, - child: FittedBox( + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 15, + ), child: Text( context.translations.sendMessagePermissionError, style: _messageInputTheme.inputTextStyle, diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index f70acd52..8a22920e 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1147,7 +1147,10 @@ class _MessageListViewState extends State { }, showEditMessage: isMyMessage, showDeleteMessage: isMyMessage, - showThreadReplyMessage: !isThreadMessage, + showThreadReplyMessage: !isThreadMessage && + streamChannel?.channel.ownCapabilities + .contains(PermissionType.sendReply) == + true, showFlagButton: !isMyMessage, borderSide: borderSide, onThreadTap: _onThreadTap, diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index d48c5099..2e97c9da 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1095,7 +1095,8 @@ class _MessageWidgetState extends State showSendingIndicator: false, padding: const EdgeInsets.all(0), showReactionPickerIndicator: widget.showReactions && - (widget.message.status == MessageSendingStatus.sent), + (widget.message.status == MessageSendingStatus.sent) && + channel.ownCapabilities.contains(PermissionType.sendReaction), showPinHighlight: false, showUserAvatar: widget.message.user!.id == channel.client.state.currentUser!.id @@ -1151,7 +1152,8 @@ class _MessageWidgetState extends State showSendingIndicator: false, padding: const EdgeInsets.all(0), showReactionPickerIndicator: widget.showReactions && - (widget.message.status == MessageSendingStatus.sent), + (widget.message.status == MessageSendingStatus.sent) && + channel.ownCapabilities.contains(PermissionType.sendReaction), showPinHighlight: false, showUserAvatar: widget.message.user!.id == channel.client.state.currentUser!.id @@ -1162,7 +1164,8 @@ class _MessageWidgetState extends State messageTheme: widget.messageTheme, reverse: widget.reverse, message: widget.message, - showReactions: widget.showReactions, + showReactions: widget.showReactions && + channel.ownCapabilities.contains(PermissionType.sendReaction), ), ), ); From ea8055988e8419f2968e92a1581fa679a3155b0c Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 6 Jan 2022 16:45:52 +0530 Subject: [PATCH 066/112] added localizations --- .../stream_chat_localizations/example/lib/add_new_lang.dart | 4 ++++ .../lib/src/stream_chat_localizations_en.dart | 4 ++++ .../lib/src/stream_chat_localizations_es.dart | 4 ++++ .../lib/src/stream_chat_localizations_fr.dart | 4 ++++ .../lib/src/stream_chat_localizations_hi.dart | 3 +++ .../lib/src/stream_chat_localizations_it.dart | 4 ++++ .../lib/src/stream_chat_localizations_ja.dart | 3 +++ .../lib/src/stream_chat_localizations_ko.dart | 3 +++ 8 files changed, 29 insertions(+) diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 6fa7689b..da6adb9b 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -84,6 +84,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { return 'Pinned by ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'You don\'t have permission to send messages'; + @override String get emptyMessagesText => 'There are no messages currently'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index c1106cab..9e5955af 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -60,6 +60,10 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { return 'Pinned by ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'You don\'t have permission to send messages'; + @override String get emptyMessagesText => 'There are no messages currently'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index 49e66184..abf87000 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -61,6 +61,10 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { return 'Fijado por ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'No tienes permiso para enviar mensajes'; + @override String get emptyMessagesText => 'Actualmente no hay mensajes'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 0057b7cc..2713105f 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -61,6 +61,10 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { return 'Épinglé par ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'Vous n\'êtes pas autorisé à envoyer des messages'; + @override String get emptyMessagesText => "Il n'y a pas de messages actuellement"; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 0d7e1ec0..25fcd3e2 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -60,6 +60,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { return '${pinnedBy.name} द्वारा पिन किया गया'; } + @override + String get sendMessagePermissionError => 'आपको संदेश भेजने की अनुमति नहीं है'; + @override String get emptyMessagesText => 'वर्तमान में कोई संदेश नहीं है'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 18e961d6..0d53d035 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -60,6 +60,10 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { return 'Messo in evidenza da ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'Non hai l\'autorizzazione per inviare messaggi'; + @override String get emptyMessagesText => 'Non c\'é nessun messaggio al momento'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 8b58630d..b327ea5e 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -60,6 +60,9 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { return '${pinnedBy.name}のピン'; } + @override + String get sendMessagePermissionError => 'メッセージを送信する権限がありません'; + @override String get emptyMessagesText => '現在、メッセージはありません。'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 3693bc11..d866f0dd 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -60,6 +60,9 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { return '${pinnedBy.name}의 핀'; } + @override + String get sendMessagePermissionError => '메시지를 보낼 수 있는 권한이 없습니다'; + @override String get emptyMessagesText => '현재 메시지가 없습니다'; From 11ad0150cea1583a8536cbe90eb6fb84e099bf14 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 6 Jan 2022 17:00:18 +0530 Subject: [PATCH 067/112] added bottom sheet capabilities --- .../stream_chat_flutter/lib/src/channel_bottom_sheet.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 74661c08..92726939 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -155,7 +155,9 @@ class _ChannelBottomSheetState extends State { title: context.translations.viewInfoLabel, onTap: widget.onViewInfoTap, ), - if (!channel.isDistinct) + if (!channel.isDistinct && + channel.ownCapabilities + .contains(PermissionType.leaveChannel)) OptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -174,7 +176,9 @@ class _ChannelBottomSheetState extends State { }); }, ), - if (isOwner) + if (isOwner && + channel.ownCapabilities + .contains(PermissionType.deleteChannel)) OptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), From 8492d5ed3232532a0a9a4b9587713f66c78d9611 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 6 Jan 2022 17:03:31 +0530 Subject: [PATCH 068/112] added mute channel permission --- packages/stream_chat/lib/src/permission_type.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/stream_chat/lib/src/permission_type.dart b/packages/stream_chat/lib/src/permission_type.dart index 931f2848..993351b7 100644 --- a/packages/stream_chat/lib/src/permission_type.dart +++ b/packages/stream_chat/lib/src/permission_type.dart @@ -35,6 +35,9 @@ class PermissionType { /// User has RemoveOwnChannelMembership or UpdateChannelMembers permission static const String leaveChannel = 'leave-channel'; + /// User can mute channel + static const String muteChannel = 'mute-channel'; + /// Ability to receive read events static const String readEvents = 'read-events'; From dcbdfc32eadd34673cec6383a5ed7975203c7b6b Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 11 Jan 2022 12:48:42 +0530 Subject: [PATCH 069/112] 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 070/112] 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 071/112] 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; From bbfdb655e62943beba6ad41980316df14e0261a0 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 13 Jan 2022 17:30:36 +0530 Subject: [PATCH 072/112] fmt --- .../lib/src/message_input/message_input.dart | 211 +++++++++--------- 1 file changed, 105 insertions(+), 106 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 53e75ad8..ce7a1693 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 @@ -490,119 +490,118 @@ class MessageInputState extends State ); } return MessageValueListenableBuilder( - valueListenable: _effectiveController, - builder: (context, value, _) { - 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; - }); - } + valueListenable: _effectiveController, + builder: (context, value, _) { + 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; + }); } - }, - 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: () { - _effectiveController.clearQuotedMessage(); - _focusNode.unfocus(); - }, - ), - ], - ), - ) - else if (_effectiveController.ogAttachment != null) - OGAttachmentPreview( - attachment: _effectiveController.ogAttachment!, - onDismissPreviewPressed: () { - _effectiveController.clearOGAttachment(); - _focusNode.unfocus(); - }, - ), + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_hasQuotedMessage) 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(), + 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: () { + _effectiveController.clearQuotedMessage(); + _focusNode.unfocus(); + }, + ), + ], ), - _buildFilePickerSection(), - ], - ), + ) + else if (_effectiveController.ogAttachment != null) + OGAttachmentPreview( + attachment: _effectiveController.ogAttachment!, + onDismissPreviewPressed: () { + _effectiveController.clearOGAttachment(); + _focusNode.unfocus(); + }, + ), + 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, - ], + ), + ); + 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, + ); + }, + ); } Flex _buildTextField(BuildContext context) => Flex( From f2ab29d8c64ad7de7df52066528ec4f04c8f7997 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 13 Jan 2022 18:08:30 +0530 Subject: [PATCH 073/112] analysis --- packages/stream_chat_flutter/lib/src/channel_list_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index bf59991e..c6c3ec35 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; From 6729ef1debfea88dc5df82f1438bc95daadd77fa Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 14 Jan 2022 16:36:30 +0530 Subject: [PATCH 074/112] test fixes --- .../lib/src/extension.dart | 1 + .../lib/src/message_actions_modal.dart | 31 ++-- .../test/src/message_action_modal_test.dart | 155 +++++++++++------- .../src/message_reactions_modal_test.dart | 38 +++-- .../stream_chat_flutter/test/src/mocks.dart | 3 + 5 files changed, 136 insertions(+), 92 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 73956da6..187865f9 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -46,6 +46,7 @@ extension IterableX on Iterable { /// Useful extension for [PlatformFile] extension PlatformFileX on PlatformFile { /// Converts the [PlatformFile] into [AttachmentFile] + //ignore: avoid_redundant_argument_values AttachmentFile get toAttachmentFile => AttachmentFile( path: kIsWeb ? null : path, name: name, 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 fa5c3c04..37396832 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -28,7 +28,7 @@ class MessageActionsModal extends StatefulWidget { this.showReplyMessage = true, this.showResendMessage = true, this.showThreadReplyMessage, - this.showFlagButton = true, + this.showFlagButton, this.showPinButton, this.editMessageInputBuilder, this.reverse = false, @@ -73,13 +73,13 @@ class MessageActionsModal extends StatefulWidget { final bool showResendMessage; /// Flag for showing reply action - final bool showReplyMessage; + final bool? showReplyMessage; /// Flag for showing thread reply action final bool? showThreadReplyMessage; /// Flag for showing flag action - final bool showFlagButton; + final bool? showFlagButton; /// Flag for showing pin action final bool? showPinButton; @@ -191,14 +191,15 @@ class _MessageActionsModalState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (_userPermissions - .contains(PermissionType.quoteMessage) && - widget.showReplyMessage && - widget.message.status == MessageSendingStatus.sent) + if (widget.showReplyMessage ?? + (_userPermissions + .contains(PermissionType.quoteMessage) && + widget.message.status == + MessageSendingStatus.sent)) _buildReplyButton(context), - if ((widget.showThreadReplyMessage ?? + if (widget.showThreadReplyMessage ?? _userPermissions - .contains(PermissionType.sendReply)) && + .contains(PermissionType.sendReply) && (widget.message.status == MessageSendingStatus.sent) && widget.message.parentId == null) @@ -209,16 +210,16 @@ class _MessageActionsModalState extends State { _isMyMessage && hasEditPermission) _buildEditMessage(context), if (widget.showCopyMessage) _buildCopyButton(context), - if (_userPermissions - .contains(PermissionType.flagMessage) && - widget.showFlagButton) + if (widget.showFlagButton ?? + _userPermissions + .contains(PermissionType.flagMessage)) _buildFlagButton(context), if (widget.showPinButton ?? _userPermissions .contains(PermissionType.pinMessage)) _buildPinButton(context), if (widget.showDeleteMessage ?? - _isMyMessage && hasDeletePermission) + (_isMyMessage && hasDeletePermission)) _buildDeleteButton(context), ...widget.customActions .map((action) => _buildCustomAction( @@ -686,8 +687,8 @@ class _MessageActionsModalState extends State { void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); _userPermissions = newStreamChannel.channel.ownCapabilities; - _isMyMessage = widget.message.user!.id == - newStreamChannel.channel.client.state.currentUser!.id; + _isMyMessage = + widget.message.user?.id == StreamChat.of(context).currentUser?.id; super.didChangeDependencies(); } } 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 92d0a6a6..72b08757 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 @@ -19,6 +19,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -32,19 +33,25 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), - status: MessageSendingStatus.sent, + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + messageTheme: streamTheme.ownMessageTheme, + showThreadReplyMessage: true, + showEditMessage: true, + showDeleteMessage: true, ), - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -66,6 +73,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -79,20 +87,23 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - showCopyMessage: false, - showReplyMessage: false, - showThreadReplyMessage: false, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + showCopyMessage: false, + showReplyMessage: false, + showThreadReplyMessage: false, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), ), - ), - messageTheme: streamTheme.ownMessageTheme, - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), ), ), ), @@ -115,6 +126,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -130,24 +142,27 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + messageWidget: const Text('test'), + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), ), + messageTheme: streamTheme.ownMessageTheme, + customActions: [ + MessageAction( + leading: const Icon(Icons.check), + title: const Text('title'), + onTap: (m) { + tapped = true; + }, + ), + ], ), - messageTheme: streamTheme.ownMessageTheme, - customActions: [ - MessageAction( - leading: const Icon(Icons.check), - title: const Text('title'), - onTap: (m) { - tapped = true; - }, - ), - ], ), ), ), @@ -170,6 +185,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -186,19 +202,22 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - onReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + messageWidget: const Text('test'), + onReplyTap: (m) { + tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), - status: MessageSendingStatus.sent, + messageTheme: streamTheme.ownMessageTheme, ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -217,6 +236,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -233,19 +253,23 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - onThreadReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: MessageActionsModal( + messageWidget: const Text('test'), + onThreadReplyTap: (m) { + tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), - status: MessageSendingStatus.sent, + messageTheme: streamTheme.ownMessageTheme, + showThreadReplyMessage: true, ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -295,6 +319,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showEditMessage: true, ), ), ), @@ -345,6 +370,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showEditMessage: true, ), ), ), @@ -545,6 +571,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -601,6 +628,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -657,6 +685,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -711,6 +740,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showDeleteMessage: true, ), ), ), @@ -767,6 +797,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showDeleteMessage: true, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart index 3732ca0d..14877ce7 100644 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart @@ -13,6 +13,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); @@ -33,13 +34,16 @@ void main() { home: StreamChat( client: client, streamChatThemeData: streamTheme, - child: MessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), + child: StreamChannel( + channel: channel, + child: MessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + message: message, + messageTheme: streamTheme.ownMessageTheme, ), - message: message, - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -58,6 +62,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); @@ -89,16 +94,19 @@ void main() { home: StreamChat( client: client, streamChatThemeData: streamTheme, - child: MessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), + child: StreamChannel( + channel: channel, + child: MessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + message: message, + messageTheme: streamTheme.ownMessageTheme, + reverse: true, + showReactions: false, + onUserAvatarTap: onUserAvatarTap, ), - message: message, - messageTheme: streamTheme.ownMessageTheme, - reverse: true, - showReactions: false, - onUserAvatarTap: onUserAvatarTap, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index 443f770c..1fa1e99b 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -21,6 +21,9 @@ class MockChannel extends Mock implements Channel { Future keyStroke([String? parentId]) async { return; } + + @override + List get ownCapabilities => ['send-message']; } class MockChannelState extends Mock implements ChannelClientState { From fbe933b0f281c02dfc9b135bbc7c787f2f09d858 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 14 Jan 2022 16:42:15 +0530 Subject: [PATCH 075/112] fmt --- .../lib/src/message_actions_modal.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 37396832..f72acf2c 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -198,11 +198,11 @@ class _MessageActionsModalState extends State { MessageSendingStatus.sent)) _buildReplyButton(context), if (widget.showThreadReplyMessage ?? - _userPermissions + _userPermissions .contains(PermissionType.sendReply) && - (widget.message.status == - MessageSendingStatus.sent) && - widget.message.parentId == null) + (widget.message.status == + MessageSendingStatus.sent) && + widget.message.parentId == null) _buildThreadReplyButton(context), if (widget.showResendMessage) _buildResendMessage(context), From cd50ee50d2cec01dcc98c9c889f581ebdb100ead Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 14 Jan 2022 20:06:56 +0530 Subject: [PATCH 076/112] fmt --- packages/stream_chat_flutter/lib/src/extension.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 187865f9..754ca8bf 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -46,8 +46,8 @@ extension IterableX on Iterable { /// Useful extension for [PlatformFile] extension PlatformFileX on PlatformFile { /// Converts the [PlatformFile] into [AttachmentFile] - //ignore: avoid_redundant_argument_values AttachmentFile get toAttachmentFile => AttachmentFile( + // ignore: avoid_redundant_argument_values path: kIsWeb ? null : path, name: name, bytes: bytes, From b2e20b8a08d3e78115f1af9dd2cfd9f8a362674a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 17 Jan 2022 12:13:03 +0100 Subject: [PATCH 077/112] add send-links capability with translation --- .../lib/src/localization/translations.dart | 13 ++++++++++ .../lib/src/message_input/message_input.dart | 25 ++++++++++++++++--- .../example/lib/add_new_lang.dart | 7 ++++++ .../lib/src/stream_chat_localizations_en.dart | 7 ++++++ .../lib/src/stream_chat_localizations_es.dart | 7 ++++++ .../lib/src/stream_chat_localizations_fr.dart | 7 ++++++ .../lib/src/stream_chat_localizations_hi.dart | 7 ++++++ .../lib/src/stream_chat_localizations_it.dart | 7 ++++++ .../lib/src/stream_chat_localizations_ja.dart | 6 +++++ .../lib/src/stream_chat_localizations_ko.dart | 6 +++++ 10 files changed, 88 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 5dfaf60b..ed15dad8 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -144,6 +144,12 @@ abstract class Translations { /// The label for "OK" String get okLabel; + /// The label for a link disabled error + String get linkDisabledError; + + /// The additional info on a link disabled error + String get linkDisabledDetails; + /// The label for "add more files" String get addMoreFilesLabel; @@ -692,4 +698,11 @@ class DefaultTranslations implements Translations { @override String attachmentLimitExceedError(int limit) => """ Attachment limit exceeded: it's not possible to add more than $limit attachments"""; + + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; } 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 ce7a1693..fde57160 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 @@ -928,7 +928,9 @@ class MessageInputState extends State _actionsShrunk = value.isNotEmpty && actionsLength > 1; }); - _checkContainsUrl(value, context); + if (channel.ownCapabilities.contains(PermissionType.sendLinks)) { + _checkContainsUrl(value, context); + } _checkCommands(value, context); _checkMentions(value, context); _checkEmoji(value, context); @@ -1646,9 +1648,25 @@ class MessageInputState extends State /// Sends the current message Future sendMessage() async { - final skipEnrichUrl = _effectiveController.ogAttachment == null; - + final streamChannel = StreamChannel.of(context); var message = _effectiveController.value; + if (!streamChannel.channel.ownCapabilities + .contains(PermissionType.sendLinks) && + _urlRegex.hasMatch(message.text ?? '')) { + showInfoDialog( + context, + icon: StreamSvgIcon.error( + color: StreamChatTheme.of(context).colorTheme.accentError, + size: 24, + ), + title: 'Links are disabled', + details: 'Sending links is not allowed in this conversation.', + okText: context.translations.okLabel, + ); + return; + } + + final skipEnrichUrl = _effectiveController.ogAttachment == null; var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; @@ -1660,7 +1678,6 @@ class MessageInputState extends State message = await widget.preMessageSending!(message); } - final streamChannel = StreamChannel.of(context); final channel = streamChannel.channel; if (!channel.state!.isUpToDate) { await streamChannel.reloadChannel(); diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index da6adb9b..d111ead0 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -396,6 +396,13 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; } void main() async { diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 9e5955af..08dbb02a 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -372,4 +372,11 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index abf87000..50c748cb 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -378,4 +378,11 @@ No es posible añadir más de $limit archivos adjuntos @override String get slowModeOnLabel => 'Modo lento activado'; + + @override + String get linkDisabledDetails => + 'No se permite enviar enlaces en esta conversación.'; + + @override + String get linkDisabledError => 'Los enlaces están deshabilitados'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 2713105f..0671f5af 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -377,4 +377,11 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ @override String get slowModeOnLabel => 'Mode lent activé'; + + @override + String get linkDisabledDetails => + 'L\'envoi de liens n\'est pas autorisé dans cette conversation.'; + + @override + String get linkDisabledError => 'Les liens sont désactivés'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 25fcd3e2..b8d3f3dc 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -371,4 +371,11 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'स्लो मोड चालू'; + + @override + String get linkDisabledDetails => + 'इस बातचीत में लिंक भेजने की अनुमति नहीं है.'; + + @override + String get linkDisabledError => 'लिंक भेजना प्रतिबंधित'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 0d53d035..1f7fd048 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -374,4 +374,11 @@ Attenzione: il limite massimo di $limit file è stato superato. @override String get slowModeOnLabel => 'Slowmode attiva'; + + @override + String get linkDisabledDetails => + 'Non è permesso condividere link in questa convesazione.'; + + @override + String get linkDisabledError => 'I links sono disattivati'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index b327ea5e..1659f637 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -357,4 +357,10 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String attachmentLimitExceedError(int limit) => ''' 添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません '''; + + @override + String get linkDisabledDetails => 'この会話では、リンクの送信は許可されていません。'; + + @override + String get linkDisabledError => 'リンクが無効になっています'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index d866f0dd..820d58e4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -357,4 +357,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String attachmentLimitExceedError(int limit) => '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; + + @override + String get linkDisabledDetails => '이 대화에서는 링크를 보낼 수 없습니다.'; + + @override + String get linkDisabledError => '링크가 비활성화되었습니다.'; } From 9b0fb58f1ed0245529c794914346ea77512eeeb6 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 17 Jan 2022 17:07:08 +0100 Subject: [PATCH 078/112] add tld check --- .../lib/src/message_input/message_input.dart | 13 ++++++++----- 1 file changed, 8 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 fde57160..816a4aec 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 @@ -928,9 +928,7 @@ class MessageInputState extends State _actionsShrunk = value.isNotEmpty && actionsLength > 1; }); - if (channel.ownCapabilities.contains(PermissionType.sendLinks)) { - _checkContainsUrl(value, context); - } + _checkContainsUrl(value, context); _checkCommands(value, context); _checkMentions(value, context); _checkEmoji(value, context); @@ -971,7 +969,11 @@ class MessageInputState extends State ..removeWhere((it) => it.group(0)?.split('.').last.isValidTLD() == false); // Reset the og attachment if the text doesn't contain any url - if (matchedUrls.isEmpty) { + if (matchedUrls.isEmpty || + !StreamChannel.of(context) + .channel + .ownCapabilities + .contains(PermissionType.sendLinks)) { _effectiveController ..text = value ..clearOGAttachment(); @@ -1652,7 +1654,8 @@ class MessageInputState extends State var message = _effectiveController.value; if (!streamChannel.channel.ownCapabilities .contains(PermissionType.sendLinks) && - _urlRegex.hasMatch(message.text ?? '')) { + _urlRegex.allMatches(message.text ?? '').any((element) => + element.group(0)?.split('.').last.isValidTLD() == true)) { showInfoDialog( context, icon: StreamSvgIcon.error( From 3c9f87ea2ba6a17ebfa7b80a4b093c965319bda9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 18 Jan 2022 10:48:42 +0100 Subject: [PATCH 079/112] fix(llc): fix truncate channel payload --- .../stream_chat/lib/src/core/api/channel_api.dart | 1 + .../test/src/core/api/channel_api_test.dart | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 63cf5458..3285ae32 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -266,6 +266,7 @@ class ChannelApi { ) async { final response = await _client.post( '${_getChannelUrl(channelId, channelType)}/truncate', + data: {}, ); return EmptyResponse.fromJson(response.data); } diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index e427ff2e..f008f8b3 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -483,14 +483,21 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/truncate'; - when(() => client.post(path)).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: {}, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); final res = await channelApi.truncateChannel(channelId, channelType); expect(res, isNotNull); - verify(() => client.post(path)).called(1); + verify(() => client.post( + path, + data: {}, + )).called(1); verifyNoMoreInteractions(client); }); From 96fae22655d8f15ba0815eddddd69d209d80ebe0 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 18 Jan 2022 16:39:07 +0100 Subject: [PATCH 080/112] update example --- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../example/lib/tutorial_part_4.dart | 16 +++++++++++++--- .../lib/src/paged_value_notifier.dart | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 1721e6f1..4b1a8d5d 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..3db53b6e 100644 --- a/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/stream_chat_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, ), ); } diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart index 5ef37374..525e663d 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -123,7 +123,7 @@ abstract class PagedValue with _$PagedValue { /// Returns `true` if the [PagedValue] is [Success] and has an error. bool get hasError => asSuccess.error != null; - /// + /// int get itemCount { final count = asSuccess.items.length; if (hasNextPage || hasError) return count + 1; From 77d105616d95f52bb2785cbd6fc7973cfb242322 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 25 Jan 2022 15:36:19 +0100 Subject: [PATCH 081/112] fix(llc): use local ownCapabilities for `channel.updated` events --- packages/stream_chat/lib/src/client/channel.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 1693aeff..6a8a13ec 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1569,7 +1569,9 @@ class ChannelClientState { _subscriptions.add(_channel.on(EventType.channelUpdated).listen((Event e) { final channel = e.channel!; updateChannelState(channelState.copyWith( - channel: channel, + channel: channel.copyWith( + ownCapabilities: channelState.channel?.ownCapabilities, + ), members: channel.members, )); })); From 5dc389783c99746eff7129fcad29ab4624f8521e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 28 Jan 2022 16:38:19 +0530 Subject: [PATCH 082/112] added properties --- .../lib/src/message_input/message_input.dart | 27 ++++++++++++- .../src/message_input/simple_safe_area.dart | 26 +++++++++++++ .../lib/src/theme/message_input_theme.dart | 39 +++++++++++++++++-- 3 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.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 816a4aec..1470856a 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/simple_safe_area.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'; @@ -215,6 +216,9 @@ class MessageInput extends StatefulWidget { this.shouldKeepFocusAfterMessage, this.validator = _defaultValidator, this.restorationId, + this.enableSafeArea, + this.elevation, + this.shadow, }) : super(key: key); /// List of options for showing overlays. @@ -331,6 +335,15 @@ class MessageInput extends StatefulWidget { /// Restoration ID to save and restore the state of the MessageInput. final String? restorationId; + /// Wrap [MessageInput] with a [SafeArea widget] + final bool? enableSafeArea; + + /// Elevation of the [MessageInput] + final double? elevation; + + /// Shadow for the [MessageInput] widget + final BoxShadow? shadow; + static bool _defaultValidator(Message message) => message.text?.isNotEmpty == true || message.attachments.isNotEmpty; @@ -495,8 +508,16 @@ class MessageInputState extends State Widget child = DecoratedBox( decoration: BoxDecoration( color: _messageInputTheme.inputBackgroundColor, + boxShadow: widget.shadow == null + ? (_streamChatTheme.messageInputTheme.shadow == null + ? [] + : [_streamChatTheme.messageInputTheme.shadow!]) + : [widget.shadow!], ), - child: SafeArea( + child: SimpleSafeArea( + enabled: widget.enableSafeArea ?? + _streamChatTheme.messageInputTheme.enableSafeArea ?? + true, child: GestureDetector( onPanUpdate: (details) { if (details.delta.dy > 0) { @@ -568,7 +589,9 @@ class MessageInputState extends State ); if (!_isEditing) { child = Material( - elevation: 8, + elevation: widget.elevation ?? + _streamChatTheme.messageInputTheme.elevation ?? + 8, child: child, ); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart new file mode 100644 index 00000000..3ae3c122 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class SimpleSafeArea extends StatefulWidget { + final bool enabled; + final Widget child; + + const SimpleSafeArea({ + Key? key, + this.enabled = true, + required this.child, + }) : super(key: key); + + @override + _SimpleSafeAreaState createState() => _SimpleSafeAreaState(); +} + +class _SimpleSafeAreaState extends State { + @override + Widget build(BuildContext context) => SafeArea( + left: widget.enabled, + top: widget.enabled, + right: widget.enabled, + bottom: widget.enabled, + child: widget.child, + ); +} 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 1348af37..9bc8ffd2 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 @@ -66,6 +66,9 @@ class MessageInputThemeData with Diagnosticable { this.borderRadius, this.expandButtonColor, this.linkHighlightColor, + this.enableSafeArea, + this.elevation, + this.shadow, }); /// Duration of the [MessageInput] send button animation @@ -107,6 +110,15 @@ class MessageInputThemeData with Diagnosticable { /// Border radius of [MessageInput] final BorderRadius? borderRadius; + /// Wrap [MessageInput] with a [SafeArea widget] + final bool? enableSafeArea; + + /// Elevation of the [MessageInput] + final double? elevation; + + /// Shadow for the [MessageInput] widget + final BoxShadow? shadow; + /// Returns a new [MessageInputThemeData] replacing some of its properties MessageInputThemeData copyWith({ Duration? sendAnimationDuration, @@ -122,6 +134,9 @@ class MessageInputThemeData with Diagnosticable { Gradient? activeBorderGradient, Gradient? idleBorderGradient, BorderRadius? borderRadius, + bool? enableSafeArea, + double? elevation, + BoxShadow? shadow, }) => MessageInputThemeData( sendAnimationDuration: @@ -139,6 +154,9 @@ class MessageInputThemeData with Diagnosticable { idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, borderRadius: borderRadius ?? this.borderRadius, linkHighlightColor: linkHighlightColor ?? this.linkHighlightColor, + enableSafeArea: enableSafeArea ?? this.enableSafeArea, + elevation: elevation ?? this.elevation, + shadow: shadow ?? this.shadow, ); /// Linearly interpolate from one [MessageInputThemeData] to another. @@ -169,6 +187,9 @@ class MessageInputThemeData with Diagnosticable { inputDecoration: a.inputDecoration, linkHighlightColor: Color.lerp(a.linkHighlightColor, b.linkHighlightColor, t), + enableSafeArea: a.enableSafeArea, + elevation: Tween(begin: a.elevation, end: b.elevation).transform(t), + shadow: BoxShadow.lerp(a.shadow, b.shadow, t), ); /// Merges [this] [MessageInputThemeData] with the [other] @@ -190,6 +211,9 @@ class MessageInputThemeData with Diagnosticable { borderRadius: other.borderRadius, expandButtonColor: other.expandButtonColor, linkHighlightColor: other.linkHighlightColor, + enableSafeArea: other.enableSafeArea, + elevation: other.elevation, + shadow: other.shadow, ); } @@ -210,7 +234,10 @@ class MessageInputThemeData with Diagnosticable { idleBorderGradient == other.idleBorderGradient && activeBorderGradient == other.activeBorderGradient && borderRadius == other.borderRadius && - linkHighlightColor == other.linkHighlightColor; + linkHighlightColor == other.linkHighlightColor && + enableSafeArea == other.enableSafeArea && + elevation == other.elevation && + shadow == other.shadow; @override int get hashCode => @@ -226,7 +253,10 @@ class MessageInputThemeData with Diagnosticable { idleBorderGradient.hashCode ^ activeBorderGradient.hashCode ^ borderRadius.hashCode ^ - linkHighlightColor.hashCode; + linkHighlightColor.hashCode ^ + elevation.hashCode ^ + shadow.hashCode ^ + enableSafeArea.hashCode; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -244,6 +274,9 @@ class MessageInputThemeData with Diagnosticable { ..add(DiagnosticsProperty('idleBorderGradient', idleBorderGradient)) ..add(DiagnosticsProperty('borderRadius', borderRadius)) ..add(ColorProperty('expandButtonColor', expandButtonColor)) - ..add(ColorProperty('linkHighlightColor', linkHighlightColor)); + ..add(ColorProperty('linkHighlightColor', linkHighlightColor)) + ..add(DiagnosticsProperty('elevation', elevation)) + ..add(DiagnosticsProperty('shadow', shadow)) + ..add(DiagnosticsProperty('enableSafeArea', enableSafeArea)); } } From 9edf02588ddfc4a8b27f149ce1c3385279164b0c Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 28 Jan 2022 16:39:57 +0530 Subject: [PATCH 083/112] docs --- .../lib/src/message_input/simple_safe_area.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart index 3ae3c122..c047ec58 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart @@ -1,15 +1,17 @@ import 'package:flutter/material.dart'; +/// A [SafeArea] with an enabled toggle class SimpleSafeArea extends StatefulWidget { - final bool enabled; - final Widget child; - + /// Constructor for [SimpleSafeArea] const SimpleSafeArea({ Key? key, this.enabled = true, required this.child, }) : super(key: key); + final bool enabled; + final Widget child; + @override _SimpleSafeAreaState createState() => _SimpleSafeAreaState(); } From 675d33281609be8a91049b45c76b8ee31a7b1f1f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 28 Jan 2022 16:40:37 +0530 Subject: [PATCH 084/112] docs --- .../lib/src/message_input/simple_safe_area.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart index c047ec58..b91684ed 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart @@ -9,7 +9,10 @@ class SimpleSafeArea extends StatefulWidget { required this.child, }) : super(key: key); + /// Wrap [child] with [SafeArea] final bool enabled; + + /// Child widget to wrap final Widget child; @override From 0524d171f41232a35d6766187735920f9af6521f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 28 Jan 2022 16:42:19 +0530 Subject: [PATCH 085/112] changelog --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 67a45ddc..6ecc7e60 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -14,6 +14,7 @@ ✅ Added - Videos can now be auto-played in `FullScreenMedia` +- Extra customisation options for `MessageInput` 🔄 Changed From 449247bc4c77f7a1e49df56980bcf0fdf7c92a18 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 28 Jan 2022 15:23:54 +0100 Subject: [PATCH 086/112] chore(repo): update melos in ci --- .github/workflows/stream_flutter_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index cc093a31..c5d4dbe0 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -3,7 +3,7 @@ name: stream_flutter_workflow env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' flutter_version: "2.5.1" - melos_version: "1.0.0-dev.10" + melos_version: "1.2.0" on: pull_request: From 4690106ff9b1c5eeabdc3a67e14e7e768cefdd34 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 28 Jan 2022 15:59:19 +0100 Subject: [PATCH 087/112] chore(llc): use path dependency for stream_chat example --- packages/stream_chat/example/pubspec.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/example/pubspec.yaml b/packages/stream_chat/example/pubspec.yaml index cf1f3874..f3fad2d1 100644 --- a/packages/stream_chat/example/pubspec.yaml +++ b/packages/stream_chat/example/pubspec.yaml @@ -11,7 +11,8 @@ dependencies: cupertino_icons: ^1.0.0 flutter: sdk: flutter - stream_chat: ^2.2.1 + stream_chat: + path: ../ dev_dependencies: flutter_test: From 291365187b7734cac78a1edee5e3b099b7f07f22 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 28 Jan 2022 16:46:14 +0100 Subject: [PATCH 088/112] chore(llc): enable verbose mode --- .github/workflows/stream_flutter_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index c5d4dbe0..c8cd0c16 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -34,7 +34,7 @@ jobs: run: | flutter pub global activate melos ${{ env.melos_version }} - name: "Bootstrap Workspace" - run: melos bootstrap + run: melos bootstrap --verbose - name: "Dart Analyze" run: | melos run analyze From 281c0455196453bba531a3206a7a820ad1e301e3 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 28 Jan 2022 17:13:05 +0100 Subject: [PATCH 089/112] chore(repo): bump flutter version --- .github/workflows/stream_flutter_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index c8cd0c16..20a76230 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -2,7 +2,7 @@ name: stream_flutter_workflow env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - flutter_version: "2.5.1" + flutter_version: "2.8.1" melos_version: "1.2.0" on: From 99af4ab8e2b2836141c4fb357a05b6ba57d2b786 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 31 Jan 2022 12:52:43 +0530 Subject: [PATCH 090/112] feat(llc): Add `queryBannedUsers` endpoint. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 15 ++++ .../stream_chat/lib/src/client/client.dart | 12 +++ .../lib/src/core/api/moderation_api.dart | 24 +++++- .../lib/src/core/api/responses.dart | 13 +++ .../lib/src/core/api/responses.g.dart | 9 +++ .../lib/src/core/models/banned_user.dart | 80 +++++++++++++++++++ .../lib/src/core/models/banned_user.g.dart | 36 +++++++++ 7 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 packages/stream_chat/lib/src/core/models/banned_user.dart create mode 100644 packages/stream_chat/lib/src/core/models/banned_user.g.dart diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 6a8a13ec..28266b14 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1290,6 +1290,21 @@ class Channel { pagination: pagination, ); + /// Query channel banned users. + Future queryBannedUsers({ + Filter? filter, + List? sort, + PaginationParams? pagination, + }) { + _checkInitialized(); + filter ??= Filter.equal('channel_cid', cid!); + return _client.queryBannedUsers( + filter: filter, + sort: sort, + pagination: pagination, + ); + } + /// Mutes the channel. Future mute({Duration? expiration}) { _checkInitialized(); diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 080a1923..52d72620 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -687,6 +687,18 @@ class StreamChatClient { return response; } + /// Query banned users. + Future queryBannedUsers({ + required Filter filter, + List? sort, + PaginationParams? pagination, + }) => + _chatApi.moderation.queryBannedUsers( + filter: filter, + sort: sort, + pagination: pagination, + ); + /// A message search. Future search( Filter filter, { diff --git a/packages/stream_chat/lib/src/core/api/moderation_api.dart b/packages/stream_chat/lib/src/core/api/moderation_api.dart index 533fbf63..30633458 100644 --- a/packages/stream_chat/lib/src/core/api/moderation_api.dart +++ b/packages/stream_chat/lib/src/core/api/moderation_api.dart @@ -1,5 +1,7 @@ -import 'package:stream_chat/src/core/api/responses.dart'; +import 'dart:convert'; + import 'package:stream_chat/src/core/http/stream_http_client.dart'; +import 'package:stream_chat/stream_chat.dart'; /// Defines the api dedicated to moderation operations class ModerationApi { @@ -125,4 +127,24 @@ class ModerationApi { ); return EmptyResponse.fromJson(response.data); } + + /// Queries banned users. + Future queryBannedUsers({ + Filter? filter, + List? sort, + PaginationParams? pagination, + }) async { + final response = await _client.get( + '/query_banned_users', + queryParameters: { + 'payload': jsonEncode({ + if (sort != null) 'sort': sort, + if (filter != null) 'filter_conditions': filter, + if (pagination != null) ...pagination.toJson(), + }), + }, + ); + + return QueryBannedUsersResponse.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 937e0150..d608c5aa 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -1,6 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/client/client.dart'; import 'package:stream_chat/src/core/error/error.dart'; +import 'package:stream_chat/src/core/models/banned_user.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/device.dart'; @@ -106,6 +107,18 @@ class QueryUsersResponse extends _BaseResponse { _$QueryUsersResponseFromJson(json); } +/// Model response for [StreamChatClient.queryBannedUsers] api call +@JsonSerializable(createToJson: false) +class QueryBannedUsersResponse extends _BaseResponse { + /// List of users returned by the query + @JsonKey(defaultValue: []) + late List bans; + + /// Create a new instance from a json + static QueryBannedUsersResponse fromJson(Map json) => + _$QueryBannedUsersResponseFromJson(json); +} + /// Model response for [channel.getReactions] api call @JsonSerializable(createToJson: false) class QueryReactionsResponse extends _BaseResponse { 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 fbcdd252..991b39f6 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -62,6 +62,15 @@ QueryUsersResponse _$QueryUsersResponseFromJson(Map json) => .toList() ?? []; +QueryBannedUsersResponse _$QueryBannedUsersResponseFromJson( + Map json) => + QueryBannedUsersResponse() + ..duration = json['duration'] as String? + ..bans = (json['bans'] as List?) + ?.map((e) => BannedUser.fromJson(e as Map)) + .toList() ?? + []; + QueryReactionsResponse _$QueryReactionsResponseFromJson( Map json) => QueryReactionsResponse() diff --git a/packages/stream_chat/lib/src/core/models/banned_user.dart b/packages/stream_chat/lib/src/core/models/banned_user.dart new file mode 100644 index 00000000..d5576615 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/banned_user.dart @@ -0,0 +1,80 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/user.dart'; + +part 'banned_user.g.dart'; + +/// Contains information about a [User] that was banned from a [Channel] or App. +@JsonSerializable() +class BannedUser extends Equatable { + /// Creates a new instance of [BannedUser] + const BannedUser({ + required this.user, + this.bannedBy, + this.channel, + this.createdAt, + this.expires, + this.shadow = false, + this.reason, + }); + + /// Create a new instance from a json + factory BannedUser.fromJson(Map json) => + _$BannedUserFromJson(json); + + /// Banned user. + final User user; + + /// User that banned the [user]. + final User? bannedBy; + + /// Channel where the [user] was banned. + final ChannelModel? channel; + + /// Timestamp when the [user] was banned. + final DateTime? createdAt; + + /// Timestamp when the [user] will be unbanned. + final DateTime? expires; + + /// Whether the [user] is a shadow banned user. + final bool shadow; + + /// Reason for the ban. + final String? reason; + + /// Serialize to json + Map toJson() => _$BannedUserToJson(this); + + /// Returns a copy of this object with the given fields updated. + BannedUser copyWith({ + User? user, + User? bannedBy, + ChannelModel? channel, + DateTime? createdAt, + DateTime? expires, + bool? shadow, + String? reason, + }) => + BannedUser( + user: user ?? this.user, + bannedBy: bannedBy ?? this.bannedBy, + channel: channel ?? this.channel, + createdAt: createdAt ?? this.createdAt, + expires: expires ?? this.expires, + shadow: shadow ?? this.shadow, + reason: reason ?? this.reason, + ); + + @override + List get props => [ + user, + bannedBy, + channel, + createdAt, + expires, + shadow, + reason, + ]; +} diff --git a/packages/stream_chat/lib/src/core/models/banned_user.g.dart b/packages/stream_chat/lib/src/core/models/banned_user.g.dart new file mode 100644 index 00000000..1f2a335b --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/banned_user.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'banned_user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BannedUser _$BannedUserFromJson(Map json) => BannedUser( + user: User.fromJson(json['user'] as Map), + bannedBy: json['banned_by'] == null + ? null + : User.fromJson(json['banned_by'] as Map), + channel: json['channel'] == null + ? null + : ChannelModel.fromJson(json['channel'] as Map), + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + expires: json['expires'] == null + ? null + : DateTime.parse(json['expires'] as String), + shadow: json['shadow'] as bool? ?? false, + reason: json['reason'] as String?, + ); + +Map _$BannedUserToJson(BannedUser instance) => + { + 'user': instance.user.toJson(), + 'banned_by': instance.bannedBy?.toJson(), + 'channel': instance.channel?.toJson(), + 'created_at': instance.createdAt?.toIso8601String(), + 'expires': instance.expires?.toIso8601String(), + 'shadow': instance.shadow, + 'reason': instance.reason, + }; From e1bdb07128929f6f0762a3a32f040b8183225cb6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 31 Jan 2022 12:53:38 +0530 Subject: [PATCH 091/112] feat(llc): deprecate `channel.banUser` in favor of `channel.banMember`. Signed-off-by: xsahil03x --- packages/stream_chat/lib/src/client/channel.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 28266b14..7d026020 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1318,9 +1318,17 @@ class Channel { } /// Bans the user with given [userID] from the channel. + @Deprecated("Use 'banMember' instead") Future banUser( String userID, Map options, + ) => + banMember(userID, options); + + /// Bans the member with given [userID] from the channel. + Future banMember( + String userID, + Map options, ) async { _checkInitialized(); final opts = Map.from(options) @@ -1332,7 +1340,11 @@ class Channel { } /// Remove the ban for the user with given [userID] in the channel. - Future unbanUser(String userID) async { + @Deprecated("Use 'unbanMember' instead") + Future unbanUser(String userID) => unbanMember(userID); + + /// Remove the ban for the member with given [userID] in the channel. + Future unbanMember(String userID) async { _checkInitialized(); return _client.unbanUser(userID, { 'type': type, From d82c533c30782f1039dff555e786a900ccf5d29a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 31 Jan 2022 12:54:20 +0530 Subject: [PATCH 092/112] fix(llc): Update channel state when member gets banned/unbanned. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 52 +++++++++++++++++++ .../lib/src/core/models/member.dart | 7 +++ .../lib/src/core/models/member.g.dart | 4 ++ .../lib/src/core/models/own_user.dart | 4 ++ .../lib/src/core/models/own_user.g.dart | 3 ++ .../stream_chat/lib/src/core/models/user.dart | 16 ++++-- .../lib/src/core/models/user.g.dart | 6 ++- packages/stream_chat/lib/src/event_type.dart | 6 +++ 8 files changed, 94 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 7d026020..52409dda 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1515,6 +1515,10 @@ class ChannelClientState { _listenMemberRemoved(); + _listenMemberBanned(); + + _listenMemberUnbanned(); + _startCleaning(); _startCleaningPinnedMessages(); @@ -1615,6 +1619,54 @@ class ChannelClientState { })); } + void _listenMemberBanned() { + _subscriptions.add(_channel + .on(EventType.userBanned) + .where((it) => it.cid != null) // filters channel ban from app ban + .listen( + (event) async { + final user = event.user!; + final member = await _channel + .queryMembers(filter: Filter.equal('id', user.id)) + .then((it) => it.members.first); + + _updateMember(member); + }, + )); + } + + void _listenMemberUnbanned() { + _subscriptions.add(_channel + .on(EventType.userUnbanned) + .where((it) => it.cid != null) // filters channel ban from app ban + .listen( + (event) async { + final user = event.user!; + final member = await _channel + .queryMembers(filter: Filter.equal('id', user.id)) + .then((it) => it.members.first); + + _updateMember(member); + }, + )); + } + + void _updateMember(Member member) { + final currentMembers = [...members]; + final memberIndex = currentMembers.indexWhere( + (m) => m.userId == member.userId, + ); + + if (memberIndex == -1) return; + currentMembers[memberIndex] = member; + + updateChannelState( + channelState.copyWith( + members: currentMembers, + ), + ); + } + /// Flag which indicates if [ChannelClientState] contain latest/recent messages or not. /// /// This flag should be managed by UI sdks. diff --git a/packages/stream_chat/lib/src/core/models/member.dart b/packages/stream_chat/lib/src/core/models/member.dart index 0dae7c04..e8b0ecff 100644 --- a/packages/stream_chat/lib/src/core/models/member.dart +++ b/packages/stream_chat/lib/src/core/models/member.dart @@ -20,6 +20,7 @@ class Member extends Equatable { DateTime? createdAt, DateTime? updatedAt, this.banned = false, + this.banExpires, this.shadowBanned = false, }) : createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(); @@ -56,6 +57,9 @@ class Member extends Equatable { /// True if the member is banned from the channel final bool banned; + /// The date at which the ban will expire. + final DateTime? banExpires; + /// True if the member is shadow banned from the channel final bool shadowBanned; @@ -77,6 +81,7 @@ class Member extends Equatable { DateTime? createdAt, DateTime? updatedAt, bool? banned, + DateTime? banExpires, bool? shadowBanned, }) => Member( @@ -85,6 +90,7 @@ class Member extends Equatable { inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, invited: invited ?? this.invited, banned: banned ?? this.banned, + banExpires: banExpires ?? this.banExpires, shadowBanned: shadowBanned ?? this.shadowBanned, role: role ?? this.role, userId: userId ?? this.userId, @@ -106,6 +112,7 @@ class Member extends Equatable { userId, isModerator, banned, + banExpires, shadowBanned, createdAt, updatedAt, diff --git a/packages/stream_chat/lib/src/core/models/member.g.dart b/packages/stream_chat/lib/src/core/models/member.g.dart index 0e711b1a..4da3cfd2 100644 --- a/packages/stream_chat/lib/src/core/models/member.g.dart +++ b/packages/stream_chat/lib/src/core/models/member.g.dart @@ -27,6 +27,9 @@ Member _$MemberFromJson(Map json) => Member( ? null : DateTime.parse(json['updated_at'] as String), banned: json['banned'] as bool? ?? false, + banExpires: json['ban_expires'] == null + ? null + : DateTime.parse(json['ban_expires'] as String), shadowBanned: json['shadow_banned'] as bool? ?? false, ); @@ -39,6 +42,7 @@ Map _$MemberToJson(Member instance) => { 'user_id': instance.userId, 'is_moderator': instance.isModerator, 'banned': instance.banned, + 'ban_expires': instance.banExpires?.toIso8601String(), 'shadow_banned': instance.shadowBanned, 'created_at': instance.createdAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(), diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 9077431b..5246f48e 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -26,6 +26,7 @@ class OwnUser extends User { bool online = false, Map extraData = const {}, bool banned = false, + DateTime? banExpires, List teams = const [], String? language, }) : super( @@ -39,6 +40,7 @@ class OwnUser extends User { online: online, extraData: extraData, banned: banned, + banExpires: banExpires, teams: teams, language: language, ); @@ -75,6 +77,7 @@ class OwnUser extends User { bool? online, Map? extraData, bool? banned, + DateTime? banExpires, List? teams, List? channelMutes, List? devices, @@ -91,6 +94,7 @@ class OwnUser extends User { // if null, it will be retrieved from extraData['image'] image: image, banned: banned ?? this.banned, + banExpires: banExpires ?? this.banExpires, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, lastActive: lastActive ?? this.lastActive, diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index 41b6e2f5..be8f2d45 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -35,6 +35,9 @@ OwnUser _$OwnUserFromJson(Map json) => OwnUser( online: json['online'] as bool? ?? false, extraData: json['extra_data'] as Map? ?? const {}, banned: json['banned'] as bool? ?? false, + banExpires: json['ban_expires'] == null + ? null + : DateTime.parse(json['ban_expires'] as String), teams: (json['teams'] as List?)?.map((e) => e as String).toList() ?? const [], diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index c6f62b6b..83b2a502 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -41,6 +41,7 @@ class User extends Equatable { Map extraData = const {}, this.online = false, this.banned = false, + this.banExpires, this.teams = const [], this.language, }) : createdAt = createdAt ?? DateTime.now(), @@ -67,6 +68,8 @@ class User extends Equatable { 'last_active', 'online', 'banned', + 'ban_expires', + 'dashboard_ban_channel_cid', 'teams', 'language', ]; @@ -129,14 +132,18 @@ class User extends Equatable { ) final bool banned; - /// Map of custom user extraData. - @JsonKey(includeIfNull: false) - final Map extraData; + /// The date at which the ban will expire. + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + final DateTime? banExpires; /// The language this user prefers. @JsonKey(includeIfNull: false) final String? language; + /// Map of custom user extraData. + @JsonKey(includeIfNull: false) + final Map extraData; + /// List of users to list of userIds. static List? toIds(List? users) => users?.map((u) => u.id).toList(); @@ -158,6 +165,7 @@ class User extends Equatable { bool? online, Map? extraData, bool? banned, + DateTime? banExpires, List? teams, String? language, }) => @@ -174,6 +182,7 @@ class User extends Equatable { online: online ?? this.online, extraData: extraData ?? this.extraData, banned: banned ?? this.banned, + banExpires: banExpires ?? this.banExpires, teams: teams ?? this.teams, language: language ?? this.language, ); @@ -186,6 +195,7 @@ class User extends Equatable { online, extraData, banned, + banExpires, teams, language, ]; diff --git a/packages/stream_chat/lib/src/core/models/user.g.dart b/packages/stream_chat/lib/src/core/models/user.g.dart index 82c799a0..9e6ae3dc 100644 --- a/packages/stream_chat/lib/src/core/models/user.g.dart +++ b/packages/stream_chat/lib/src/core/models/user.g.dart @@ -21,6 +21,9 @@ User _$UserFromJson(Map json) => User( extraData: json['extra_data'] as Map? ?? const {}, online: json['online'] as bool? ?? false, banned: json['banned'] as bool? ?? false, + banExpires: json['ban_expires'] == null + ? null + : DateTime.parse(json['ban_expires'] as String), teams: (json['teams'] as List?)?.map((e) => e as String).toList() ?? const [], @@ -45,7 +48,8 @@ Map _$UserToJson(User instance) { writeNotNull('last_active', readonly(instance.lastActive)); writeNotNull('online', readonly(instance.online)); writeNotNull('banned', readonly(instance.banned)); - val['extra_data'] = instance.extraData; + writeNotNull('ban_expires', readonly(instance.banExpires)); writeNotNull('language', instance.language); + val['extra_data'] = instance.extraData; return val; } diff --git a/packages/stream_chat/lib/src/event_type.dart b/packages/stream_chat/lib/src/event_type.dart index 87529399..80628f2f 100644 --- a/packages/stream_chat/lib/src/event_type.dart +++ b/packages/stream_chat/lib/src/event_type.dart @@ -73,6 +73,12 @@ class EventType { /// Event sent when a member is removed to a channel static const String memberRemoved = 'member.removed'; + /// Event sent when a member is removed to a channel + static const String userBanned = 'user.banned'; + + /// Event sent when a member is removed to a channel + static const String userUnbanned = 'user.unbanned'; + /// Event sent when a channel is hidden static const String channelHidden = 'channel.hidden'; From 3b00fecaec4ee1450c35031db8db0a21957fc4d0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 31 Jan 2022 13:10:02 +0530 Subject: [PATCH 093/112] chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 655491cf..dad87973 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,14 +1,22 @@ ## Upcoming +🐞 Fixed + +- [[#857]](https://github.com/GetStream/stream-chat-flutter/issues/857) Channel now listens for member ban/unban and + updates the channel state with the latest data. + 🔄 Changed -- `client.location` is now deprecated in favor of the new [edge server](https://getstream.io/blog/chat-edge-infrastructure) and will be removed in v4.0.0. +- `client.location` is now deprecated in favor of the + new [edge server](https://getstream.io/blog/chat-edge-infrastructure) and will be removed in v4.0.0. +- `channel.banUser`, `channel.unbanUser` is now deprecated in favor of the new `channel.banMember` + and `channel.unbanMember` and will be removed in v4.0.0. ✅ Added - Added `client.enrichUrl` endpoint for enriching URLs with metadata. - Fixed `unreadCount` after removing user from a channel. -- `ChannelModel` now supplies individual user capabilities. +- Added `client.queryBannedUsers`, `channel.queryBannedUsers` endpoint for querying banned users. ## 3.3.1 From 7e2a1846d832aecbb0ae0112ad9cb5d1bcd96dce Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 31 Jan 2022 12:35:29 +0100 Subject: [PATCH 094/112] chore(repo): fix analysis --- packages/stream_chat/lib/src/client/channel.dart | 6 ------ .../stream_chat/lib/src/core/error/stream_chat_error.dart | 1 - .../stream_chat/lib/src/core/models/attachment_file.dart | 1 - packages/stream_chat/test/src/client/channel_test.dart | 5 ++--- .../stream_chat/test/src/client/retry_queue_test.dart | 1 - .../stream_chat/test/src/core/api/channel_api_test.dart | 2 -- .../stream_chat/test/src/core/api/responses_test.dart | 6 ------ packages/stream_chat/test/src/core/api/user_api_test.dart | 2 -- .../test/src/core/models/channel_state_test.dart | 5 ----- packages/stream_chat/test/src/core/models/event_test.dart | 2 -- .../stream_chat/test/src/core/models/own_user_test.dart | 2 -- packages/stream_chat/test/src/fakes.dart | 1 - packages/stream_chat_flutter/example/lib/main.dart | 2 -- .../lib/src/attachment/attachment_title.dart | 2 -- .../lib/src/attachment/attachment_widget.dart | 1 - .../lib/src/attachment/giphy_attachment.dart | 2 -- .../lib/src/attachment/image_attachment.dart | 3 --- .../lib/src/attachment/url_attachment.dart | 1 - .../lib/src/attachment/video_attachment.dart | 2 -- .../lib/src/attachment_actions_modal.dart | 3 --- packages/stream_chat_flutter/lib/src/back_button.dart | 2 -- packages/stream_chat_flutter/lib/src/channel_avatar.dart | 1 - packages/stream_chat_flutter/lib/src/channel_header.dart | 5 ----- packages/stream_chat_flutter/lib/src/channel_info.dart | 1 - .../stream_chat_flutter/lib/src/channel_list_header.dart | 4 ---- .../stream_chat_flutter/lib/src/channel_list_view.dart | 4 ---- packages/stream_chat_flutter/lib/src/channel_name.dart | 2 -- packages/stream_chat_flutter/lib/src/channel_preview.dart | 4 ---- .../lib/src/connection_status_builder.dart | 1 - .../stream_chat_flutter/lib/src/full_screen_media.dart | 2 -- packages/stream_chat_flutter/lib/src/gallery_footer.dart | 3 --- packages/stream_chat_flutter/lib/src/gradient_avatar.dart | 1 - packages/stream_chat_flutter/lib/src/image_group.dart | 3 --- packages/stream_chat_flutter/lib/src/media_list_view.dart | 1 - .../lib/src/message_actions_modal.dart | 8 -------- .../stream_chat_flutter/lib/src/message_list_view.dart | 8 -------- .../lib/src/message_reactions_modal.dart | 5 ----- .../stream_chat_flutter/lib/src/message_search_item.dart | 2 -- .../lib/src/message_search_list_view.dart | 4 ---- packages/stream_chat_flutter/lib/src/message_text.dart | 2 -- packages/stream_chat_flutter/lib/src/message_widget.dart | 8 -------- .../lib/src/quoted_message_widget.dart | 2 -- packages/stream_chat_flutter/lib/src/reaction_bubble.dart | 3 --- packages/stream_chat_flutter/lib/src/stream_chat.dart | 4 ---- .../stream_chat_flutter/lib/src/stream_chat_theme.dart | 7 ------- packages/stream_chat_flutter/lib/src/stream_svg_icon.dart | 1 - packages/stream_chat_flutter/lib/src/thread_header.dart | 2 -- .../stream_chat_flutter/lib/src/unread_indicator.dart | 1 - packages/stream_chat_flutter/lib/src/user_avatar.dart | 1 - packages/stream_chat_flutter/lib/src/user_item.dart | 4 ---- packages/stream_chat_flutter/lib/src/user_list_view.dart | 2 -- packages/stream_chat_flutter/lib/src/utils.dart | 1 - .../stream_chat_flutter/lib/src/visible_footnote.dart | 1 - .../horizontal_scrollable_positioned_list_test.dart | 1 - .../reversed_scrollable_positioned_list_test.dart | 1 - .../scrollable_positioned_list_test.dart | 2 -- .../separated_scrollable_positioned_list_test.dart | 1 - ...erated_horizontal_scrollable_positioned_list_test.dart | 1 - .../stream_chat_flutter/test/src/back_button_test.dart | 1 - .../test/src/gradient_avatar_test.dart | 1 - .../test/src/message_action_modal_test.dart | 1 - packages/stream_chat_flutter/test/utils/golden.dart | 1 - .../lib/src/stream_chat_core.dart | 1 - .../stream_chat_flutter_core/lib/src/user_list_core.dart | 2 -- .../test/message_search_bloc_test.dart | 2 -- .../test/stream_channel_test.dart | 3 --- .../test/stream_chat_core_test.dart | 1 - .../lib/src/stream_chat_localizations_hi.dart | 4 ++-- .../lib/src/stream_chat_persistence_client.dart | 1 - 69 files changed, 4 insertions(+), 170 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 52409dda..3dc879cb 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -4,15 +4,9 @@ import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension, ListEquality; import 'package:dio/dio.dart'; -import 'package:rate_limiter/rate_limiter.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/retry_queue.dart'; -import 'package:stream_chat/src/core/error/error.dart'; -import 'package:stream_chat/src/core/models/attachment_file.dart'; -import 'package:stream_chat/src/core/models/channel_state.dart'; -import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/util/utils.dart'; -import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/stream_chat.dart'; /// Class that manages a specific channel. diff --git a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart index ce234c84..bebea751 100644 --- a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart +++ b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:stream_chat/src/core/error/chat_error_code.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.dart b/packages/stream_chat/lib/src/core/models/attachment_file.dart index e855f7e3..71cdec29 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.dart @@ -3,7 +3,6 @@ import 'dart:typed_data'; import 'package:dio/dio.dart' show MultipartFile; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:http_parser/http_parser.dart'; -import 'package:meta/meta.dart'; import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; import 'package:stream_chat/src/core/util/extension.dart'; diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 7eb68b79..ce392053 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -1,5 +1,4 @@ import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/client/channel.dart'; import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; @@ -2066,7 +2065,7 @@ void main() { {'type': channelType, 'id': channelId, ...options}, )).thenAnswer((_) async => EmptyResponse()); - final res = await channel.banUser(userId, options); + final res = await channel.banMember(userId, options); expect(res, isNotNull); @@ -2082,7 +2081,7 @@ void main() { when(() => client.unbanUser(userId, any())) .thenAnswer((_) async => EmptyResponse()); - final res = await channel.unbanUser(userId); + final res = await channel.unbanMember(userId); expect(res, isNotNull); diff --git a/packages/stream_chat/test/src/client/retry_queue_test.dart b/packages/stream_chat/test/src/client/retry_queue_test.dart index 084b82ec..a04feb3e 100644 --- a/packages/stream_chat/test/src/client/retry_queue_test.dart +++ b/packages/stream_chat/test/src/client/retry_queue_test.dart @@ -4,7 +4,6 @@ import 'package:stream_chat/src/client/retry_queue.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/event_type.dart'; -import 'package:test/scaffolding.dart'; import 'package:test/test.dart'; import '../mocks.dart'; diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index f008f8b3..2403b44e 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -3,8 +3,6 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; -import 'package:stream_chat/src/core/models/channel_model.dart'; -import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/core/api/responses_test.dart b/packages/stream_chat/test/src/core/api/responses_test.dart index a61d825b..8a73fccd 100644 --- a/packages/stream_chat/test/src/core/api/responses_test.dart +++ b/packages/stream_chat/test/src/core/api/responses_test.dart @@ -1,11 +1,5 @@ import 'dart:convert'; -import 'package:stream_chat/src/core/api/responses.dart'; -import 'package:stream_chat/src/core/models/device.dart'; -import 'package:stream_chat/src/core/models/member.dart'; -import 'package:stream_chat/src/core/models/message.dart'; -import 'package:stream_chat/src/core/models/reaction.dart'; -import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/core/api/user_api_test.dart b/packages/stream_chat/test/src/core/api/user_api_test.dart index 6eecbee1..dbf83f2c 100644 --- a/packages/stream_chat/test/src/core/api/user_api_test.dart +++ b/packages/stream_chat/test/src/core/api/user_api_test.dart @@ -2,9 +2,7 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/core/api/requests.dart'; import 'package:stream_chat/src/core/api/user_api.dart'; -import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/core/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index 8bd17d46..58f810a4 100644 --- a/packages/stream_chat/test/src/core/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_state_test.dart @@ -1,8 +1,3 @@ -import 'package:stream_chat/src/core/models/channel_config.dart'; -import 'package:stream_chat/src/core/models/channel_state.dart'; -import 'package:stream_chat/src/core/models/command.dart'; -import 'package:stream_chat/src/core/models/message.dart'; -import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/core/models/event_test.dart b/packages/stream_chat/test/src/core/models/event_test.dart index 0cd9a0ad..40ebc044 100644 --- a/packages/stream_chat/test/src/core/models/event_test.dart +++ b/packages/stream_chat/test/src/core/models/event_test.dart @@ -1,5 +1,3 @@ -import 'package:stream_chat/src/core/models/event.dart'; -import 'package:stream_chat/src/core/models/own_user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/core/models/own_user_test.dart b/packages/stream_chat/test/src/core/models/own_user_test.dart index ea84185f..430e9c36 100644 --- a/packages/stream_chat/test/src/core/models/own_user_test.dart +++ b/packages/stream_chat/test/src/core/models/own_user_test.dart @@ -1,6 +1,4 @@ import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/core/models/own_user.dart'; -import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index 3dcd8ebd..b4089512 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 3d292b09..6b5a299f 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -1,5 +1,3 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart'; diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index df134250..f1369d49 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Title for attachments class AttachmentTitle extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart index 7a5c891b..75a890e8 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Enum for identifying type of attachment enum AttachmentSource { 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 cc29f0b2..1974b040 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -3,9 +3,7 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/visible_footnote.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget for showing a GIF attachment class GiphyAttachment extends AttachmentWidget { 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 ff01eea3..a46ff25c 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -2,11 +2,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; -import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget for showing an image attachment class ImageAttachment extends AttachmentWidget { diff --git a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart index d56bcd6a..8a79ca21 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -1,6 +1,5 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Widget to display URL attachment 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 0fc97d5e..3df1f451 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; -import 'package:stream_chat_flutter/src/full_screen_media.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; 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 ac9213b6..4fedfc12 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -1,7 +1,4 @@ -import 'dart:ui'; - import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:path_provider/path_provider.dart'; diff --git a/packages/stream_chat_flutter/lib/src/back_button.dart b/packages/stream_chat_flutter/lib/src/back_button.dart index b0b04b8d..08d8dabf 100644 --- a/packages/stream_chat_flutter/lib/src/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/back_button.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/unread_indicator.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Back button implementation diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index 59b4a73a..ba24a640 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -2,7 +2,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/group_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image_paint.png) diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 62f994dc..954aa758 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -1,13 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:stream_chat_flutter/src/back_button.dart'; import 'package:stream_chat_flutter/src/channel_info.dart'; -import 'package:stream_chat_flutter/src/channel_name.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/info_tile.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png) diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 791805af..0238e0f5 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 3ebd9001..6c54ad89 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -1,12 +1,8 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget builder for title typedef TitleBuilder = Widget Function( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index c6c3ec35..e4097690 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -1,13 +1,9 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Callback called when tapping on a channel typedef ChannelTapCallback = void Function(Channel, Widget?); diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 258893c8..a1e81005 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// It shows the current [Channel] name using a [Text] widget. /// diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index de038c82..125d06fe 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -1,12 +1,8 @@ import 'package:collection/collection.dart' show IterableExtension, ListEquality; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png) diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index 2cba8eb4..f0eca9da 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget that builds itself based on the latest snapshot of interaction with /// a [Stream] of type [ConnectionStatus]. 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 84ddb803..83ae96b1 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -6,8 +6,6 @@ import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/gallery_footer.dart'; -import 'package:stream_chat_flutter/src/gallery_header.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index 0acd9b02..f189dae3 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -6,11 +6,8 @@ import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/theme/themes.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'; /// Footer widget for media display class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart index a02976c8..12824f45 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -1,5 +1,4 @@ import 'dart:math'; -import 'dart:ui'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index 88b309fe..ff67b068 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -1,8 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/full_screen_media.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget for constructing a group of images in message class ImageGroup extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/media_list_view.dart b/packages/stream_chat_flutter/lib/src/media_list_view.dart index 8b1f7f06..d5a9233c 100644 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/media_list_view.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:photo_manager/photo_manager.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; extension on Duration { 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 f72acf2c..154472d7 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -1,15 +1,7 @@ import 'dart:ui'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/message_action.dart'; -import 'package:stream_chat_flutter/src/reaction_picker.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Constructs a modal with actions for a message class MessageActionsModal extends StatefulWidget { diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 8a22920e..a75f9317 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -2,20 +2,12 @@ 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:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/info_tile.dart'; -import 'package:stream_chat_flutter/src/message_widget.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/swipeable.dart'; -import 'package:stream_chat_flutter/src/system_message.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:visibility_detector/visibility_detector.dart'; /// Widget builder for message diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 37c8afff..7568c565 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -3,12 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; -import 'package:stream_chat_flutter/src/reaction_picker.dart'; -import 'package:stream_chat_flutter/src/stream_chat.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; -import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Modal widget for displaying message reactions class MessageReactionsModal extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 07f9f1c4..022ec7ce 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// It shows the current [Message] preview. /// diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index a5edc203..76ae0e98 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/info_tile.dart'; -import 'package:stream_chat_flutter/src/message_search_item.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Callback called when tapping on a user typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 6cdcd760..bb2946f4 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -1,9 +1,7 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Text widget to display in message class MessageText extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 2e97c9da..864c977e 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -1,21 +1,13 @@ -import 'dart:ui'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_portal/flutter_portal.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/attachment/url_attachment.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/image_group.dart'; -import 'package:stream_chat_flutter/src/message_action.dart'; import 'package:stream_chat_flutter/src/message_actions_modal.dart'; import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Widget builder for building attachments diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 14abc121..131ca60b 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -1,9 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:video_player/video_player.dart'; /// Widget builder for quoted message attachment thumnail diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index 203b8481..dd3d5e27 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -2,9 +2,6 @@ import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/reaction_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Creates reaction bubble widget for displaying over messages diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index 327a0927..cf18e282 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -1,12 +1,8 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget used to provide information about the chat to the widget tree /// 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 8a210b96..84006f44 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -1,12 +1,5 @@ -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/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'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Inherited widget providing the [StreamChatThemeData] to the widget tree class StreamChatTheme extends InheritedWidget { diff --git a/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart b/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart index 73c33131..06b3b9a4 100644 --- a/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart +++ b/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index dfe13b41..aae1d8f9 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header_paint.png) diff --git a/packages/stream_chat_flutter/lib/src/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/unread_indicator.dart index e5f3027b..958386d1 100644 --- a/packages/stream_chat_flutter/lib/src/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/unread_indicator.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Widget for showing an unread indicator diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index 86b34ea7..e638685c 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -1,7 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget that displays a user avatar class UserAvatar extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user_item.dart index cbaa0820..c0e3482e 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user_item.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/user_list_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// /// It shows the current [User] preview. diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 8697381a..58af30a7 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Callback called when tapping on a user typedef UserTapCallback = void Function(User, Widget?); diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index b4ca48f0..2d661fbc 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:url_launcher/url_launcher.dart'; /// Launch URL diff --git a/packages/stream_chat_flutter/lib/src/visible_footnote.dart b/packages/stream_chat_flutter/lib/src/visible_footnote.dart index 18bb976d..700e22ad 100644 --- a/packages/stream_chat_flutter/lib/src/visible_footnote.dart +++ b/packages/stream_chat_flutter/lib/src/visible_footnote.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Widget for displaying a footnote diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart index d32f3969..fb5c9fc2 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart index b0c487ea..a5af56a8 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart index 5c4f22f1..79845215 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart @@ -4,11 +4,9 @@ import 'dart:async'; import 'dart:math'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart'; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart index 0cc091c9..df1035e2 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart index 55574003..c26ee43f 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/stream_chat_flutter/test/src/back_button_test.dart b/packages/stream_chat_flutter/test/src/back_button_test.dart index efb889ac..fca30c5b 100644 --- a/packages/stream_chat_flutter/test/src/back_button_test.dart +++ b/packages/stream_chat_flutter/test/src/back_button_test.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/back_button.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; diff --git a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart index 6f7ebe2e..6dcc0e2d 100644 --- a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart +++ b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:golden_toolkit/golden_toolkit.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/gradient_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; 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 72b08757..69e43fba 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,7 +3,6 @@ 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'; diff --git a/packages/stream_chat_flutter/test/utils/golden.dart b/packages/stream_chat_flutter/test/utils/golden.dart index 4fc0aff3..574306ef 100644 --- a/packages/stream_chat_flutter/test/utils/golden.dart +++ b/packages/stream_chat_flutter/test/utils/golden.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index 3d00b739..1f3f753d 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.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/typedef.dart'; diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index 05e9730b..61fbb860 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -2,8 +2,6 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter_core/src/users_bloc.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// diff --git a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart index 332289ea..764e629b 100644 --- a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart @@ -1,8 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter_core/src/message_search_bloc.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'matchers/get_message_response_matcher.dart'; diff --git a/packages/stream_chat_flutter_core/test/stream_channel_test.dart b/packages/stream_chat_flutter_core/test/stream_channel_test.dart index de536cb5..cccea24d 100644 --- a/packages/stream_chat_flutter_core/test/stream_channel_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_channel_test.dart @@ -1,7 +1,4 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index addcab87..7dd433be 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index b8d3f3dc..1244e2ea 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -343,10 +343,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get sendLabel => 'भेजें'; @override - String get withText => 'विद'; //TODO: break? + String get withText => 'विद'; @override - String get inText => 'इन'; //TODO: break? + String get inText => 'इन'; @override String get youText => 'आप'; diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index 00c4ab06..2dc61f18 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart' show LogRecord; -import 'package:meta/meta.dart'; import 'package:mutex/mutex.dart'; import 'package:stream_chat/stream_chat.dart'; From 1bcea053ad3b5b84a966f0df5fb934edd68e73fa Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 31 Jan 2022 13:34:33 +0100 Subject: [PATCH 095/112] chore(repo): fix metrics --- .github/workflows/dart_code_metrics.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dart_code_metrics.yaml b/.github/workflows/dart_code_metrics.yaml index 667ac2dd..63ec9484 100644 --- a/.github/workflows/dart_code_metrics.yaml +++ b/.github/workflows/dart_code_metrics.yaml @@ -1,8 +1,9 @@ name: Dart Code Metrics env: - flutter_version: "2.5.0" + flutter_version: "2.8.1" folders: "lib, test" + melos_version: "1.2.0" on: pull_request: @@ -33,7 +34,7 @@ jobs: flutter-version: ${{ env.flutter_version }} - name: "Install Tools" - run: flutter pub global activate melos 1.0.0-dev.10 + run: flutter pub global activate melos ${{ env.melos_version }} - name: "Bootstrap Workspace" run: melos bootstrap From 6c624b72b690d8741078a9ef9674091a78bda746 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 26 Jan 2022 15:20:07 +0530 Subject: [PATCH 096/112] fix(llc): include `message.user` while saving users in persistence. Signed-off-by: xsahil03x --- packages/stream_chat/lib/src/db/chat_persistence_client.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index 98f6a364..82ba181a 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -253,6 +253,7 @@ abstract class ChatPersistenceClient { users.addAll([ channel.createdBy, + ...messages.map((it) => it.user), ...reads.map((it) => it.user), ...members.map((it) => it.user), ...reactions.map((it) => it.user), From 03bd2e11c99113a1f01ff825d384f32b2e930907 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 26 Jan 2022 15:22:43 +0530 Subject: [PATCH 097/112] chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index dad87973..e47e72cb 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -4,6 +4,7 @@ - [[#857]](https://github.com/GetStream/stream-chat-flutter/issues/857) Channel now listens for member ban/unban and updates the channel state with the latest data. +- [[#748]](https://github.com/GetStream/stream-chat-flutter/issues/748) `Message.user` are now also included while saving users in persistence. 🔄 Changed From c80957ebae41e8c0311f3f6181792d5c1431c3e5 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 24 Jan 2022 18:03:02 +0530 Subject: [PATCH 098/112] fix: thread message deletion --- packages/stream_chat/CHANGELOG.md | 4 ++++ packages/stream_chat/lib/src/client/channel.dart | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index e47e72cb..7afc4000 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -19,6 +19,10 @@ - Fixed `unreadCount` after removing user from a channel. - Added `client.queryBannedUsers`, `channel.queryBannedUsers` endpoint for querying banned users. +🐞 Fixed + +- [[#871]](https://github.com/GetStream/stream-chat-flutter/issues/871) Fixed thread message deletion. + ## 3.3.1 🐞 Fixed diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 3dc879cb..460cef3f 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1827,7 +1827,13 @@ class ChannelClientState { if (replyCount == null || replyCount == 0) return; addMessage(parentMessage.copyWith(replyCount: replyCount - 1)); - updateThreadInfo(parentId, threads[parentId]!..remove(message)); + updateThreadInfo( + parentId, + threads[parentId]! + ..removeWhere( + (e) => e.id == message.id, + ), + ); } else { // Remove regular message final allMessages = [...messages]; From bc8784f59354cd73a260b8972520469aac60eafa Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 1 Feb 2022 14:08:12 +0530 Subject: [PATCH 099/112] test(llc): add tests Signed-off-by: xsahil03x --- .../test/src/client/channel_test.dart | 30 ++++++++++++++++++ .../test/src/client/client_test.dart | 31 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index ce392053..49310d05 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -1,5 +1,6 @@ import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/client/retry_policy.dart'; +import 'package:stream_chat/src/core/models/banned_user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; @@ -1982,6 +1983,35 @@ void main() { )).called(1); }); + test('`.queryBannedUsers`', () async { + final filter = Filter.equal('channel_cid', channelCid); + + final bans = List.generate( + 3, + (index) => BannedUser( + user: User(id: 'test-user-id-$index'), + bannedBy: User(id: 'test-user-id-${index + 1}'), + ), + ); + + when(() => client.queryBannedUsers( + filter: filter, + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).thenAnswer((_) async => QueryBannedUsersResponse()..bans = bans); + + final res = await channel.queryBannedUsers(); + + expect(res, isNotNull); + expect(res.bans.length, bans.length); + + verify(() => client.queryBannedUsers( + filter: filter, + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).called(1); + }); + test('`.mute`', () async { when(() => client.muteChannel( channelCid, diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 22e0edf8..b89db0dc 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -1,6 +1,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/models/banned_user.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; @@ -982,6 +983,36 @@ void main() { verifyNoMoreInteractions(api.user); }); + test('`.queryBannedUsers`', () async { + final bans = List.generate( + 3, + (index) => BannedUser( + user: User(id: 'test-user-id-$index'), + bannedBy: User(id: 'test-user-id-${index + 1}'), + ), + ); + + const cid = 'message:nice-channel'; + final filter = Filter.equal('channel_cid', cid); + + when(() => api.moderation.queryBannedUsers( + filter: filter, + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).thenAnswer((_) async => QueryBannedUsersResponse()..bans = bans); + + final res = await client.queryBannedUsers(filter: filter); + expect(res, isNotNull); + expect(res.bans.length, bans.length); + + verify(() => api.moderation.queryBannedUsers( + filter: filter, + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + )).called(1); + verifyNoMoreInteractions(api.moderation); + }); + test('`.search`', () async { const cid = 'test-type:test-id'; final filter = Filter.in_('cid', const [cid]); From f53ee0b4f10a71390a16f0a12e53aff1f5143461 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Feb 2022 10:52:59 +0100 Subject: [PATCH 100/112] fix analysis and update examples --- .../example/lib/split_view.dart | 34 +++++---- .../example/lib/tutorial_part_1.dart | 25 +++---- .../example/lib/tutorial_part_2.dart | 54 ++++++-------- .../example/lib/tutorial_part_3.dart | 59 ++++++++++----- .../example/lib/tutorial_part_4.dart | 56 +++++++------- .../example/lib/tutorial_part_5.dart | 46 ++++++++---- .../example/lib/tutorial_part_6.dart | 74 +++++++++++-------- .../lib/src/group_avatar.dart | 1 + .../lib/src/paged_value_notifier.dart | 8 +- .../stream_channel_list_controller.dart | 8 +- .../stream_channel_list_event_handler.dart | 4 +- .../stream_channel_list_tile.dart | 3 +- .../stream_channel_list_view.dart | 6 +- .../v4/stream_channel_info_bottom_sheet.dart | 5 +- .../lib/stream_chat_flutter.dart | 4 + 15 files changed, 223 insertions(+), 164 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 1fd3ccb6..75b8f64b 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -84,7 +84,7 @@ class _SplitViewState extends State { ); } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, this.onTap, @@ -92,22 +92,26 @@ class ChannelListPage extends StatelessWidget { final void Function(Channel)? onTap; + @override + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + @override Widget build(BuildContext context) => Scaffold( - body: ChannelsBloc( - child: ChannelListView( - onChannelTap: onTap != null - ? (channel, _) { - onTap!(channel); - } - : null, - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - ), + body: StreamChannelListView( + onChannelTap: widget.onTap, + controller: _listController, ), ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index 459d8690..82acabf9 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -90,18 +90,15 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: const [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index f704abab..9f718b7e 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -92,26 +92,23 @@ class _ChannelListPageState extends State { ); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: RefreshIndicator( - onRefresh: _controller.refresh, - child: StreamChannelListView( - controller: _controller, - onChannelTap: (channel) => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => StreamChannel( - channel: channel, - child: const ChannelPage(), + Widget build(BuildContext context) => Scaffold( + body: RefreshIndicator( + onRefresh: _controller.refresh, + child: StreamChannelListView( + controller: _controller, + onChannelTap: (channel) => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), ), ), ), ), - ), - ); - } + ); } class ChannelPage extends StatelessWidget { @@ -120,18 +117,15 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: const [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 538dbd68..e8834eb1 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -68,31 +68,49 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - channelPreviewBuilder: _channelPreviewBuilder, - // sort: [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), - ), - ), - ); - } + State createState() => _ChannelListPageState(); +} - Widget _channelPreviewBuilder(BuildContext context, Channel channel) { +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + itemBuilder: _channelPreviewBuilder, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, + ), + ); + + Widget _channelPreviewBuilder( + BuildContext context, + Channel channel, + StreamChannelListTile defaultTile, + ) { final lastMessage = channel.state?.messages.reversed.firstWhereOrNull( (message) => !message.isDeleted, ); @@ -115,13 +133,14 @@ class ChannelListPage extends StatelessWidget { leading: ChannelAvatar( channel: channel, ), - title: ChannelName( + title: StreamChannelName( textStyle: ChannelPreviewTheme.of(context).titleStyle!.copyWith( color: StreamChatTheme.of(context) .colorTheme .textHighEmphasis .withOpacity(opacity), ), + channel: channel, ), subtitle: Text(subtitle), trailing: channel.state!.unreadCount > 0 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 5b80b721..ce461ddc 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -53,38 +53,42 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: StreamChannelListView( - controller: StreamChannelListController( - client: StreamChat.of(context).client, - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - ), - onChannelTap: (channel) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: const ChannelPage(), + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), ), - ), - ); - }, - ), - ); - } + ); + }, + ), + ); } class ChannelPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index 86ac2e1f..347ceaf4 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -59,28 +59,42 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, ), - ), - ); - } + ); } class ChannelPage extends StatelessWidget { 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 2555e007..4bb8e928 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -93,28 +93,41 @@ class MyApp extends StatelessWidget { } } -class ChannelListPage extends StatelessWidget { +class ChannelListPage extends StatefulWidget { const ChannelListPage({ Key? key, }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - body: ChannelsBloc( - child: ChannelListView( - filter: Filter.in_( - 'members', - [StreamChat.of(context).currentUser!.id], - ), - sort: const [SortOption('last_message_at')], - limit: 20, - channelWidget: const ChannelPage(), + State createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + late final _listController = StreamChannelListController( + client: StreamChat.of(context).client, + filter: Filter.in_( + 'members', + [StreamChat.of(context).currentUser!.id], + ), + sort: const [SortOption('last_message_at')], + limit: 20, + ); + @override + Widget build(BuildContext context) => Scaffold( + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, ), - ), - ); - } + ); } class ChannelPage extends StatelessWidget { @@ -123,24 +136,21 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: [ - Expanded( - child: MessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: [ + Expanded( + child: MessageListView( + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, + ), ), ), - ), - const MessageInput(), - ], - ), - ); - } + const MessageInput(), + ], + ), + ); } class ThreadPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/group_avatar.dart index d7996c4d..e0e87e23 100644 --- a/packages/stream_chat_flutter/lib/src/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/group_avatar.dart @@ -16,6 +16,7 @@ class GroupAvatar extends StatelessWidget { this.selectionThickness = 4, }) : super(key: key); + /// The channel of the avatar final Channel? channel; /// List of images to display diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart index 525e663d..9525fea4 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart @@ -5,8 +5,10 @@ import 'package:stream_chat/stream_chat.dart' show StreamChatError; part 'paged_value_notifier.freezed.dart'; +/// Default initial page size multiplier. const defaultInitialPagedLimitMultiplier = 3; +/// Value listenable for paged data. typedef PagedValueListenableBuilder = ValueListenableBuilder>; @@ -57,6 +59,7 @@ abstract class PagedValueNotifier final nextPageKey = lastValue.nextPageKey; // resetting the error value = lastValue.copyWith(error: null); + // ignore: null_check_on_nullable_type_parameter return loadMore(nextPageKey!); } @@ -78,10 +81,9 @@ abstract class PagedValueNotifier Future loadMore(Key nextPageKey); } +/// Paged value that can be used with [PagedValueNotifier]. @freezed abstract class PagedValue with _$PagedValue { - const PagedValue._(); - /// Represents the success state of the [PagedValue] // @Assert( // 'nextPageKey != null', @@ -98,6 +100,8 @@ abstract class PagedValue with _$PagedValue { StreamChatError? error, }) = Success; + const PagedValue._(); + /// Represents the loading state of the [PagedValue]. const factory PagedValue.loading() = Loading; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart index 488452bd..e7eb903e 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:stream_chat/stream_chat.dart' hide Success; import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; @@ -7,6 +8,8 @@ import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list /// The default channel page limit to load. const defaultChannelPagedLimit = 10; +const _kDefaultBackendPaginationLimit = 30; + /// A controller for a Channel list. /// /// This class lets you perform tasks such as: @@ -102,7 +105,10 @@ class StreamChannelListController extends PagedValueNotifier { @override Future doInitialLoad() async { - final limit = this.limit * defaultInitialPagedLimitMultiplier; + final limit = min( + this.limit * defaultInitialPagedLimitMultiplier, + _kDefaultBackendPaginationLimit, + ); try { await for (final channels in client.queryChannels( filter: filter, diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart index 449dd982..44ace69e 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart @@ -146,8 +146,8 @@ class StreamChannelListEventHandler { /// Function which gets called for the event /// [EventType.notificationMessageNew]. /// - /// This event is fired when a new message is created in a channel which we are - /// not currently watching. + /// This event is fired when a new message is created in a channel + /// which we are not currently watching. /// /// By default, this adds the channel and moves it to the top of list. void onNotificationMessageNew( diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index 3c392a8f..f820e040 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -68,7 +68,8 @@ class StreamChannelListTile extends StatelessWidget { /// {@template flutter.material.ListTile.tileColor} /// Defines the background color of `ListTile`. /// - /// When the value is null, the `tileColor` is set to [ListTileTheme.tileColor] + /// When the value is null, + /// the `tileColor` is set to [ListTileTheme.tileColor] /// if it's not null and to [Colors.transparent] if it's null. /// {@endtemplate} final Color? tileColor; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index 1527493e..adf69be0 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -143,9 +143,9 @@ class StreamChannelListView extends StatefulWidget { /// only scroll the view if it has sufficient content. See [physics]. /// /// Also when true, the scroll view is used for default [ScrollAction]s. If a - /// ScrollAction is not handled by an otherwise focused part of the application, - /// the ScrollAction will be evaluated using this scroll view, for example, - /// when executing [Shortcuts] key events like page up and down. + /// ScrollAction is not handled by an otherwise focused part of the + /// application, the ScrollAction will be evaluated using this scroll view, + /// for example, when executing [Shortcuts] key events like page up and down. /// /// On iOS, this also identifies the scroll view that will scroll to top in /// response to a tap in the status bar. diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart index 51bda67d..5107131d 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart @@ -229,8 +229,9 @@ const _kDefaultChannelInfoBottomSheetShape = RoundedRectangleBorder( /// The [transitionAnimationController] controls the bottom sheet's entrance and /// exit animations if provided. /// -/// The optional `routeSettings` parameter sets the [RouteSettings] of the modal bottom sheet -/// sheet. This is particularly useful in the case that a user wants to observe +/// The optional `routeSettings` parameter sets the [RouteSettings] +/// of the modal bottom sheet sheet. +/// This is particularly useful in the case that a user wants to observe /// [PopupRoute]s within a [NavigatorObserver]. /// /// Returns a `Future` that resolves to the value (if any) that was passed to diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index fab90d17..7e6dc769 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -57,6 +57,10 @@ export 'src/utils.dart'; // v4 export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; export 'src/v4/channel_list_view/stream_channel_list_event_handler.dart'; +export 'src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; +export 'src/v4/channel_list_view/stream_channel_list_tile.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/v4/stream_channel_avatar.dart'; export 'src/v4/stream_channel_info_bottom_sheet.dart'; +export 'src/v4/stream_channel_name.dart'; export 'src/visible_footnote.dart'; From e80cebe9aa631605a6f27c70d6756b1c57f97573 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Feb 2022 11:57:26 +0100 Subject: [PATCH 101/112] deprecate old widgets --- .../example/lib/tutorial_part_3.dart | 2 +- .../example/lib/tutorial_part_4.dart | 29 +++++++++---------- .../lib/src/channel_avatar.dart | 5 ++++ .../lib/src/channel_list_view.dart | 4 +++ .../lib/src/channel_name.dart | 4 +++ 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index e8834eb1..76447b11 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -130,7 +130,7 @@ class _ChannelListPageState extends State { ), ); }, - leading: ChannelAvatar( + leading: StreamChannelAvatar( channel: channel, ), title: StreamChannelName( 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 ce461ddc..fdea19ba 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -97,24 +97,21 @@ class ChannelPage extends StatelessWidget { }) : super(key: key); @override - // ignore: prefer_expression_function_bodies - Widget build(BuildContext context) { - return Scaffold( - appBar: const ChannelHeader(), - body: Column( - children: [ - Expanded( - child: MessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: [ + Expanded( + child: MessageListView( + threadBuilder: (_, parentMessage) => ThreadPage( + parent: parentMessage, + ), ), ), - ), - const MessageInput(), - ], - ), - ); - } + const MessageInput(), + ], + ), + ); } class ThreadPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index ba24a640..50466040 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -44,6 +44,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. + +@Deprecated( + "'ChannelName' is deprecated and shouldn't be used. " + "Please use 'StreamChannelName' instead.", +) class ChannelAvatar extends StatelessWidget { /// Instantiate a new ChannelImage const ChannelAvatar({ diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index e4097690..270c29af 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -52,6 +52,10 @@ typedef ViewInfoCallback = void Function(Channel); /// The widget components render the ui based on the first ancestor of /// type [StreamChatTheme]. /// Modify it to change the widget appearance. +@Deprecated( + "'ChannelListView' is deprecated and shouldn't be used. " + "Please use 'StreamChannelListView' instead.", +) class ChannelListView extends StatefulWidget { /// Instantiate a new ChannelListView ChannelListView({ diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index a1e81005..8dd8c054 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -6,6 +6,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// /// The widget uses a [StreamBuilder] to render the channel information /// image as soon as it updates. +@Deprecated( + "'ChannelName' is deprecated and shouldn't be used. " + "Please use 'StreamChannelName' instead.", +) class ChannelName extends StatelessWidget { /// Instantiate a new ChannelName const ChannelName({ From 6351fa0a5f8aef763399eca49a20f1b9f6223ec0 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Feb 2022 12:03:29 +0100 Subject: [PATCH 102/112] deprecate old widgets --- packages/stream_chat_flutter/lib/src/channel_avatar.dart | 4 ++-- .../stream_chat_flutter/lib/src/channel_bottom_sheet.dart | 5 +++++ packages/stream_chat_flutter_core/lib/src/channels_bloc.dart | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index 50466040..f7c949ee 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -46,8 +46,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Modify it to change the widget appearance. @Deprecated( - "'ChannelName' is deprecated and shouldn't be used. " - "Please use 'StreamChannelName' instead.", + "'ChannelAvatar' is deprecated and shouldn't be used. " + "Please use 'StreamChannelAvatar' instead.", ) class ChannelAvatar extends StatelessWidget { /// Instantiate a new ChannelImage diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 92726939..05d5996e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -4,6 +4,10 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Bottom Sheet with options +@Deprecated( + "'ChannelBottomSheet' is deprecated and shouldn't be used. " + "Please use 'StreamChannelBottomSheet' instead.", +) class ChannelBottomSheet extends StatefulWidget { /// Constructor for creating bottom sheet const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key); @@ -15,6 +19,7 @@ class ChannelBottomSheet extends StatefulWidget { _ChannelBottomSheetState createState() => _ChannelBottomSheetState(); } +// ignore: deprecated_member_use_from_same_package class _ChannelBottomSheetState extends State { bool _showActions = true; 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 fa19a90f..255eeae4 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -16,6 +16,10 @@ import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; /// using Flutter's [BuildContext]. /// /// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/ +@Deprecated( + "'ChannelsBloc' is deprecated and shouldn't be used. " + "Please use 'StreamChannelListView' instead.", +) class ChannelsBloc extends StatefulWidget { /// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and /// not null. From ce22e854a811179cf63239cf87ebabbcf5346b2d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 1 Feb 2022 12:29:52 +0100 Subject: [PATCH 103/112] Update packages/stream_chat_flutter_core/lib/src/channels_bloc.dart Co-authored-by: Sahil Kumar --- packages/stream_chat_flutter_core/lib/src/channels_bloc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 255eeae4..9e7b05d1 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -18,7 +18,7 @@ import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; /// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/ @Deprecated( "'ChannelsBloc' is deprecated and shouldn't be used. " - "Please use 'StreamChannelListView' instead.", + "Please use 'StreamChannelListController' instead.", ) class ChannelsBloc extends StatefulWidget { /// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and From 202af02f8e7d6d456a66e3ddd313cb434efa53a8 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 7 Feb 2022 15:25:07 +0530 Subject: [PATCH 104/112] chore: move controllers from ui to core Signed-off-by: xsahil03x --- .../src/message_input/simple_safe_area.dart | 17 +++----- .../stream_channel_list_view.dart | 3 -- .../lib/stream_chat_flutter.dart | 4 -- .../message_input_controller_test.dart | 14 ------ .../lib/src}/message_input_controller.dart | 4 +- .../src}/message_text_field_controller.dart | 15 +------ .../lib/src/paged_value_notifier.dart | 0 .../lib/src/paged_value_notifier.freezed.dart | 43 +++++++++---------- .../src}/stream_channel_list_controller.dart | 5 ++- .../stream_channel_list_event_handler.dart | 2 +- .../lib/stream_chat_flutter_core.dart | 5 +++ .../stream_chat_flutter_core/pubspec.yaml | 3 ++ 12 files changed, 44 insertions(+), 71 deletions(-) delete mode 100644 packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart rename packages/{stream_chat_flutter/lib/src/message_input => stream_chat_flutter_core/lib/src}/message_input_controller.dart (98%) rename packages/{stream_chat_flutter/lib/src/message_input => stream_chat_flutter_core/lib/src}/message_text_field_controller.dart (82%) rename packages/{stream_chat_flutter => stream_chat_flutter_core}/lib/src/paged_value_notifier.dart (100%) rename packages/{stream_chat_flutter => stream_chat_flutter_core}/lib/src/paged_value_notifier.freezed.dart (93%) rename packages/{stream_chat_flutter/lib/src/v4/channel_list_view => stream_chat_flutter_core/lib/src}/stream_channel_list_controller.dart (98%) rename packages/{stream_chat_flutter/lib/src/v4/channel_list_view => stream_chat_flutter_core/lib/src}/stream_channel_list_event_handler.dart (98%) diff --git a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart index b91684ed..5f3d7391 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; /// A [SafeArea] with an enabled toggle -class SimpleSafeArea extends StatefulWidget { +class SimpleSafeArea extends StatelessWidget { /// Constructor for [SimpleSafeArea] const SimpleSafeArea({ Key? key, @@ -15,17 +15,12 @@ class SimpleSafeArea extends StatefulWidget { /// Child widget to wrap final Widget child; - @override - _SimpleSafeAreaState createState() => _SimpleSafeAreaState(); -} - -class _SimpleSafeAreaState extends State { @override Widget build(BuildContext context) => SafeArea( - left: widget.enabled, - top: widget.enabled, - right: widget.enabled, - bottom: widget.enabled, - child: widget.child, + left: enabled, + top: enabled, + right: enabled, + bottom: enabled, + child: child, ); } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart index adf69be0..1d88f354 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -1,11 +1,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/extension.dart'; -import 'package:stream_chat_flutter/src/paged_value_notifier.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/v4/channel_list_view/stream_channel_list_controller.dart'; import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 7e6dc769..83a31510 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -25,8 +25,6 @@ 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'; @@ -55,8 +53,6 @@ export 'src/user_mention_tile.dart'; export 'src/utils.dart'; // v4 -export 'src/v4/channel_list_view/stream_channel_list_controller.dart'; -export 'src/v4/channel_list_view/stream_channel_list_event_handler.dart'; export 'src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; export 'src/v4/channel_list_view/stream_channel_list_tile.dart'; export 'src/v4/channel_list_view/stream_channel_list_view.dart'; diff --git a/packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart deleted file mode 100644 index 1b496784..00000000 --- a/packages/stream_chat_flutter/test/src/message_input/message_input_controller_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -void main() { - testWidgets( - 'should instantiate a new MessageInputController with empty message', - (tester) async { - final controller = MessageInputController()..text = 'test'; - - expect(controller.text, 'test'); - expect(controller.message.text, 'test'); - }, - ); -} diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart similarity index 98% rename from packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart rename to packages/stream_chat_flutter_core/lib/src/message_input_controller.dart index 185c875c..65c6d8d8 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -2,7 +2,9 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.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]. /// diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart similarity index 82% rename from packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart rename to packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart index 4a6c3708..0f9f75c6 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/message_text_field_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart @@ -1,6 +1,4 @@ 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( @@ -33,17 +31,8 @@ class MessageTextFieldController extends TextEditingController { TextStyle? style, required bool withComposing, }) { - final pattern = textPatternStyle ?? - { - RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+'): - (context, text) { - if (!text.split('.').last.isValidTLD()) return null; - return TextStyle( - color: MessageInputTheme.of(context).linkHighlightColor, - ); - }, - }; - if (pattern.isEmpty) { + final pattern = textPatternStyle; + if (pattern == null || pattern.isEmpty) { return super.buildTextSpan( context: context, style: style, diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/paged_value_notifier.dart rename to packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart diff --git a/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart similarity index 93% rename from packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart rename to packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart index 515a9742..a7ea0ea3 100644 --- a/packages/stream_chat_flutter/lib/src/paged_value_notifier.freezed.dart +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart @@ -191,22 +191,20 @@ class _$Success extends Success @override bool operator ==(dynamic other) { return identical(this, other) || - (other is Success && - (identical(other.items, items) || - const DeepCollectionEquality().equals(other.items, items)) && - (identical(other.nextPageKey, nextPageKey) || - const DeepCollectionEquality() - .equals(other.nextPageKey, nextPageKey)) && - (identical(other.error, error) || - const DeepCollectionEquality().equals(other.error, error))); + (other.runtimeType == runtimeType && + other is Success && + const DeepCollectionEquality().equals(other.items, items) && + const DeepCollectionEquality() + .equals(other.nextPageKey, nextPageKey) && + const DeepCollectionEquality().equals(other.error, error)); } @override - int get hashCode => - runtimeType.hashCode ^ - const DeepCollectionEquality().hash(items) ^ - const DeepCollectionEquality().hash(nextPageKey) ^ - const DeepCollectionEquality().hash(error); + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(items), + const DeepCollectionEquality().hash(nextPageKey), + const DeepCollectionEquality().hash(error)); @JsonKey(ignore: true) @override @@ -296,13 +294,13 @@ abstract class Success extends PagedValue { const Success._() : super._(); /// List with all items loaded so far. - List get items => throw _privateConstructorUsedError; + List get items; /// The key for the next page to be fetched. - Key? get nextPageKey => throw _privateConstructorUsedError; + Key? get nextPageKey; /// The current error, if any. - StreamChatError? get error => throw _privateConstructorUsedError; + StreamChatError? get error; @JsonKey(ignore: true) $SuccessCopyWith> get copyWith => throw _privateConstructorUsedError; @@ -347,7 +345,8 @@ class _$Loading extends Loading @override bool operator ==(dynamic other) { - return identical(this, other) || (other is Loading); + return identical(this, other) || + (other.runtimeType == runtimeType && other is Loading); } @override @@ -490,14 +489,14 @@ class _$Error extends Error @override bool operator ==(dynamic other) { return identical(this, other) || - (other is Error && - (identical(other.error, error) || - const DeepCollectionEquality().equals(other.error, error))); + (other.runtimeType == runtimeType && + other is Error && + const DeepCollectionEquality().equals(other.error, error)); } @override int get hashCode => - runtimeType.hashCode ^ const DeepCollectionEquality().hash(error); + Object.hash(runtimeType, const DeepCollectionEquality().hash(error)); @JsonKey(ignore: true) @override @@ -583,7 +582,7 @@ abstract class Error extends PagedValue { const factory Error(StreamChatError error) = _$Error; const Error._() : super._(); - StreamChatError get error => throw _privateConstructorUsedError; + StreamChatError get error; @JsonKey(ignore: true) $ErrorCopyWith> get copyWith => throw _privateConstructorUsedError; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart similarity index 98% rename from packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart rename to packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index e7eb903e..7d3a281f 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -2,8 +2,9 @@ import 'dart:async'; import 'dart:math'; import 'package:stream_chat/stream_chat.dart' hide Success; -import 'package:stream_chat_flutter/src/paged_value_notifier.dart'; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_event_handler.dart'; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +import 'package:stream_chat_flutter_core/src/stream_channel_list_event_handler.dart'; /// The default channel page limit to load. const defaultChannelPagedLimit = 10; diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart similarity index 98% rename from packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart rename to packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart index 44ace69e..8d548c67 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart @@ -1,5 +1,5 @@ import 'package:stream_chat/stream_chat.dart' show ChannelState, Event; -import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_controller.dart'; +import 'package:stream_chat_flutter_core/src/stream_channel_list_controller.dart'; /// Contains handlers that are called from [StreamChannelListController] for /// certain [Event]s. 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..78d82d8b 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,10 +7,15 @@ 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; +export 'src/message_text_field_controller.dart'; +export 'src/paged_value_notifier.dart' show PagedValueListenableBuilder; export 'src/stream_channel.dart'; +export 'src/stream_channel_list_controller.dart'; +export 'src/stream_channel_list_event_handler.dart'; export 'src/stream_chat_core.dart'; export 'src/typedef.dart'; export 'src/user_list_core.dart' hide UserListCoreState; diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index fb9307c1..cf1206cc 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -14,14 +14,17 @@ dependencies: connectivity_plus: ^2.1.0 flutter: sdk: flutter + freezed_annotation: ^1.0.0 meta: ^1.3.0 rxdart: ^0.27.0 stream_chat: ^3.3.1 dev_dependencies: + build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 fake_async: ^1.2.0 flutter_test: sdk: flutter + freezed: ^1.0.0 mocktail: ^0.2.0 From 165e7a5e248000c5153e1e60a52f4fefe7f4fc96 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 7 Mar 2022 19:03:01 +0530 Subject: [PATCH 105/112] test(llc): fix tests Signed-off-by: xsahil03x --- .../test/src/client/channel_test.dart | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index fe38af5f..9452a433 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -1132,7 +1132,10 @@ void main() { test('should work fine with score passed explicitly', () async { const type = 'test-reaction-type'; - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); const score = 5; final reaction = Reaction( @@ -1192,7 +1195,10 @@ void main() { test('should work fine with score passed explicitly and in extraData', () async { const type = 'test-reaction-type'; - final message = Message(id: 'test-message-id'); + final message = Message( + id: 'test-message-id', + status: MessageSendingStatus.sent, + ); const score = 5; const extraDataScore = 3; @@ -1392,6 +1398,7 @@ void main() { final message = Message( id: 'test-message-id', parentId: 'test-parent-id', // is thread message + status: MessageSendingStatus.sent, ); final reaction = Reaction(type: type, messageId: message.id); @@ -1441,6 +1448,7 @@ void main() { final message = Message( id: 'test-message-id', parentId: 'test-parent-id', // is thread message + status: MessageSendingStatus.sent, ); final reaction = Reaction(type: type, messageId: message.id); @@ -1508,6 +1516,7 @@ void main() { latestReactions: [prevReaction], reactionScores: const {prevType: 1}, reactionCounts: const {prevType: 1}, + status: MessageSendingStatus.sent, ); const type = 'test-reaction-type-2'; @@ -1688,11 +1697,13 @@ void main() { ); final message = Message( id: messageId, - parentId: parentId, // is thread + parentId: parentId, + // is thread ownReactions: [reaction], latestReactions: [reaction], reactionScores: const {type: 1}, reactionCounts: const {type: 1}, + status: MessageSendingStatus.sent, ); when(() => client.deleteReaction(messageId, type)) @@ -1745,6 +1756,7 @@ void main() { latestReactions: [reaction], reactionScores: const {type: 1}, reactionCounts: const {type: 1}, + status: MessageSendingStatus.sent, ); when(() => client.deleteReaction(messageId, type)) From 357357cdff63630d37fd108fb5c6b3ae00b64c32 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 8 Mar 2022 10:29:49 +0100 Subject: [PATCH 106/112] fix tests --- .../lib/src/theme/message_input_theme.dart | 4 +++- .../test/src/typing_indicator_test.dart | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) 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 9bc8ffd2..81047d77 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 @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; @@ -188,7 +190,7 @@ class MessageInputThemeData with Diagnosticable { linkHighlightColor: Color.lerp(a.linkHighlightColor, b.linkHighlightColor, t), enableSafeArea: a.enableSafeArea, - elevation: Tween(begin: a.elevation, end: b.elevation).transform(t), + elevation: lerpDouble(a.elevation, b.elevation, t), shadow: BoxShadow.lerp(a.shadow, b.shadow, t), ); diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart index 8d790266..0a98aff7 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -63,19 +63,23 @@ void main() { Event(type: EventType.typingStart), })); + const typingKey = Key('typing'); + await tester.pumpWidget(MaterialApp( home: StreamChat( client: client, child: StreamChannel( channel: channel, child: const Scaffold( - body: TypingIndicator(), + body: TypingIndicator( + key: typingKey, + ), ), ), ), )); - expect(find.byKey(const Key('typings')), findsOneWidget); + expect(find.byKey(typingKey), findsOneWidget); }, ); } From f8c0f808d81698ebea7cb9782ab1409aabc86b0f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 8 Mar 2022 10:44:52 +0100 Subject: [PATCH 107/112] add missing localizations --- .../lib/src/message_list_view.dart | 1 + .../lib/src/stream_chat_localizations.dart | 1 + .../lib/src/stream_chat_localizations_pt.dart | 11 +++++++++++ 3 files changed, 13 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 2773b192..e4d67740 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -526,6 +526,7 @@ class _MessageListViewState extends State { return ((index + 2) * 2) - 1; } } + return null; }, // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index 0c7ce68e..6ea2eda4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -75,6 +75,7 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { case 'pt': return const StreamChatLocalizationsPt(); } + return null; } /// Implementation of localized strings for the stream chat widgets diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart index 44c02081..4c1d2a58 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart @@ -371,4 +371,15 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String get slowModeOnLabel => 'Modo lento ativado'; + + @override + String get linkDisabledDetails => + 'O envio de links não é permitido nesta conversa.'; + + @override + String get linkDisabledError => 'Os links estão desativados'; + + @override + String get sendMessagePermissionError => + 'Você não tem permissão para enviar mensagens'; } From a7e3839256d0c0725dcdf030766bb5e091ac0127 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 10 Mar 2022 14:12:01 +0530 Subject: [PATCH 108/112] refactor(ui, core): Deprecate v3 Signed-off-by: xsahil03x --- .../stream_chat_flutter/example/lib/main.dart | 8 +- .../example/lib/split_view.dart | 6 +- .../example/lib/tutorial_part_1.dart | 8 +- .../example/lib/tutorial_part_2.dart | 8 +- .../example/lib/tutorial_part_3.dart | 12 +- .../example/lib/tutorial_part_4.dart | 16 +- .../example/lib/tutorial_part_5.dart | 10 +- .../example/lib/tutorial_part_6.dart | 22 +- .../lib/src/attachment/attachment_title.dart | 12 +- .../attachment_upload_state_builder.dart | 16 +- .../lib/src/attachment/attachment_widget.dart | 10 +- .../lib/src/attachment/file_attachment.dart | 16 +- .../lib/src/attachment/giphy_attachment.dart | 16 +- .../lib/src/attachment/image_attachment.dart | 22 +- .../lib/src/attachment/url_attachment.dart | 16 +- .../lib/src/attachment/video_attachment.dart | 26 +- .../lib/src/attachment_actions_modal.dart | 2 +- .../lib/src/back_button.dart | 2 +- .../lib/src/channel_avatar.dart | 6 +- .../lib/src/channel_bottom_sheet.dart | 26 +- .../lib/src/channel_header.dart | 29 +- .../lib/src/channel_info.dart | 18 +- .../lib/src/channel_list_header.dart | 38 +- .../lib/src/channel_list_view.dart | 23 +- .../lib/src/channel_name.dart | 5 +- .../lib/src/channel_preview.dart | 41 +- .../lib/src/commands_overlay.dart | 13 +- .../lib/src/connection_status_builder.dart | 10 +- .../lib/src/date_divider.dart | 12 +- .../lib/src/deleted_message.dart | 16 +- .../lib/src/emoji_overlay.dart | 12 +- .../lib/src/full_screen_media.dart | 20 +- .../lib/src/gallery_footer.dart | 25 +- .../lib/src/gallery_header.dart | 15 +- .../lib/src/gradient_avatar.dart | 16 +- .../lib/src/group_avatar.dart | 17 +- .../lib/src/image_group.dart | 20 +- .../lib/src/info_tile.dart | 12 +- .../lib/src/localization/translations.dart | 24 +- .../lib/src/media_list_view.dart | 36 +- .../lib/src/mention_tile.dart | 101 - .../lib/src/message_action.dart | 12 +- .../lib/src/message_actions_modal.dart | 31 +- .../lib/src/message_input.dart | 2110 +++++++++++++++++ .../src/message_input/countdown_button.dart | 6 +- .../lib/src/message_input/message_input.dart | 52 +- .../stream_attachment_picker.dart | 4 +- .../stream_message_send_button.dart | 6 +- .../lib/src/message_list_view.dart | 46 +- .../lib/src/message_reactions_modal.dart | 22 +- .../lib/src/message_search_item.dart | 20 +- .../lib/src/message_search_list_view.dart | 27 +- .../lib/src/message_text.dart | 16 +- .../lib/src/message_widget.dart | 68 +- .../lib/src/multi_overlay.dart | 10 +- .../lib/src/option_list_tile.dart | 12 +- .../lib/src/quoted_message_widget.dart | 106 +- .../lib/src/reaction_bubble.dart | 14 +- .../lib/src/reaction_icon.dart | 10 +- .../lib/src/reaction_picker.dart | 18 +- .../lib/src/sending_indicator.dart | 12 +- .../lib/src/stream_chat_theme.dart | 162 +- .../lib/src/system_message.dart | 14 +- .../lib/src/theme/avatar_theme.dart | 32 +- .../lib/src/theme/channel_header_theme.dart | 72 +- .../src/theme/channel_list_header_theme.dart | 72 +- .../src/theme/channel_list_view_theme.dart | 66 +- .../lib/src/theme/channel_preview_theme.dart | 68 +- .../lib/src/theme/color_theme.dart | 20 +- .../lib/src/theme/gallery_footer_theme.dart | 62 +- .../lib/src/theme/gallery_header_theme.dart | 62 +- .../lib/src/theme/message_input_theme.dart | 62 +- .../src/theme/message_list_view_theme.dart | 64 +- .../theme/message_search_list_view_theme.dart | 56 +- .../lib/src/theme/message_theme.dart | 38 +- .../lib/src/theme/text_theme.dart | 20 +- .../lib/src/theme/user_list_view_theme.dart | 62 +- .../lib/src/thread_header.dart | 17 +- .../lib/src/typing_indicator.dart | 10 +- .../lib/src/unread_indicator.dart | 12 +- .../lib/src/upload_progress_indicator.dart | 12 +- .../lib/src/user_avatar.dart | 12 +- .../lib/src/user_item.dart | 19 +- .../lib/src/user_list_view.dart | 21 +- .../lib/src/user_mention_tile.dart | 14 +- .../lib/src/user_mentions_overlay.dart | 23 +- .../stream_channel_list_tile.dart | 8 +- .../lib/src/v4/stream_channel_avatar.dart | 6 +- .../v4/stream_channel_info_bottom_sheet.dart | 14 +- .../lib/src/video_service.dart | 5 + .../lib/src/video_thumbnail_image.dart | 23 +- .../lib/src/visible_footnote.dart | 12 +- .../lib/stream_chat_flutter.dart | 1 - .../test/src/attachment_widgets_test.dart | 2 +- .../test/src/back_button_test.dart | 2 +- .../test/src/channel_header_test.dart | 40 +- .../test/src/channel_image_test.dart | 3 +- .../test/src/channel_list_header_test.dart | 19 +- .../test/src/channel_preview_test.dart | 2 +- .../test/src/date_divider_test.dart | 2 +- .../test/src/deleted_message_test.dart | 10 +- .../test/src/full_screen_media_test.dart | 2 +- .../test/src/gradient_avatar_test.dart | 15 +- .../test/src/image_footer_test.dart | 2 +- .../test/src/info_tile_test.dart | 4 +- .../test/src/message_action_modal_test.dart | 34 +- .../test/src/message_input_test.dart | 4 +- .../test/src/message_list_view_test.dart | 10 +- .../src/message_reactions_modal_test.dart | 12 +- .../test/src/message_text_test.dart | 10 +- .../test/src/reaction_bubble_test.dart | 10 +- .../test/src/system_message_test.dart | 8 +- .../test/src/theme/avatar_theme_test.dart | 17 +- .../src/theme/channel_header_theme_test.dart | 38 +- .../theme/channel_list_header_theme_test.dart | 34 +- .../theme/channel_list_view_theme_test.dart | 34 +- .../src/theme/channel_preview_theme_test.dart | 52 +- .../src/theme/gallery_footer_theme_test.dart | 72 +- .../src/theme/gallery_header_theme_test.dart | 40 +- .../src/theme/message_input_theme_test.dart | 64 +- .../theme/message_list_view_theme_test.dart | 46 +- .../message_search_list_view_theme_test.dart | 35 +- .../test/src/theme/message_theme_test.dart | 75 +- .../src/theme/user_list_view_theme_test.dart | 32 +- .../test/src/thread_header_test.dart | 4 +- .../test/src/typing_indicator_test.dart | 2 +- .../test/src/unread_indicator_test.dart | 6 +- .../lib/src/channels_bloc.dart | 5 +- 128 files changed, 3891 insertions(+), 1362 deletions(-) delete mode 100644 packages/stream_chat_flutter/lib/src/mention_tile.dart create mode 100644 packages/stream_chat_flutter/lib/src/message_input.dart diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 6b5a299f..4a7eabf4 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -87,7 +87,7 @@ class MyApp extends StatelessWidget { /// A list of messages sent in the current channel. /// -/// This is implemented using [MessageListView], a widget that provides query +/// This is implemented using [StreamMessageListView], a widget that provides query /// functionalities fetching the messages from the api and showing them in a /// listView. class ChannelPage extends StatelessWidget { @@ -98,13 +98,13 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(attachmentLimit: 3), + StreamMessageInput(attachmentLimit: 3), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 75b8f64b..6f4c51e6 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -125,15 +125,15 @@ class ChannelPage extends StatelessWidget { Widget build(BuildContext context) => Navigator( onGenerateRoute: (settings) => MaterialPageRoute( builder: (context) => Scaffold( - appBar: const ChannelHeader( + appBar: const StreamChannelHeader( showBackButton: false, ), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index 82acabf9..cd3bb12e 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -27,7 +27,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// - We make [StreamChat] the root Widget of our application /// /// - We create a single [ChannelPage] widget under [StreamChat] with three -/// widgets: [ChannelHeader], [MessageListView] and [MessageInput] +/// widgets: [StreamChannelHeader], [StreamMessageListView] and [StreamMessageInput] /// /// If you now run the simulator you will see a single channel UI. void main() async { @@ -91,13 +91,13 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 9f718b7e..0ca2356e 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -25,7 +25,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The [ChannelListPage] widget retrieves the list of channels based on a /// custom query and ordering. In this case we are showing the list of /// channels in which the current user is a member and we order them based -/// on the time they had a new message. [ChannelListView] handles pagination +/// on the time they had a new message. [StreamChannelListView] handles pagination /// and updates automatically when new channels are created or when a new /// message is added to a channel. void main() async { @@ -118,13 +118,13 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 76447b11..8eb01fbb 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -15,8 +15,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// We start by changing how channel previews are shown in the channel list /// and include the number of unread messages for each. /// -/// We're passing a custom widget to [ChannelListView.channelPreviewBuilder]; -/// this will override the default [ChannelPreview] and allows you to create +/// We're passing a custom widget to [StreamChannelListView.channelPreviewBuilder]; +/// this will override the default [StreamChannelPreview] and allows you to create /// one yourself. /// /// There are a couple interesting things we do in this widget: @@ -134,7 +134,7 @@ class _ChannelListPageState extends State { channel: channel, ), title: StreamChannelName( - textStyle: ChannelPreviewTheme.of(context).titleStyle!.copyWith( + textStyle: StreamChannelPreviewTheme.of(context).titleStyle!.copyWith( color: StreamChatTheme.of(context) .colorTheme .textHighEmphasis @@ -162,13 +162,13 @@ class ChannelPage extends StatelessWidget { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: const [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ); 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 fdea19ba..89aa55f7 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -8,8 +8,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// to create sub-conversations inside the same channel. /// /// Using threaded conversations is very simple and mostly a matter of -/// plugging the [MessageListView] to another widget that renders the widget. -/// To make this simple, such a widget only needs to build [MessageListView] +/// plugging the [StreamMessageListView] to another widget that renders the widget. +/// To make this simple, such a widget only needs to build [StreamMessageListView] /// with the parent attribute set to the thread’s root message. /// /// Now we can open threads and create new ones as well. If you long-press a @@ -98,17 +98,17 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: [ Expanded( - child: MessageListView( + child: StreamMessageListView( threadBuilder: (_, parentMessage) => ThreadPage( parent: parentMessage, ), ), ), - const MessageInput(), + const StreamMessageInput(), ], ), ); @@ -126,17 +126,17 @@ class ThreadPage extends StatelessWidget { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ThreadHeader( + appBar: StreamThreadHeader( parent: parent!, ), body: Column( children: [ Expanded( - child: MessageListView( + child: StreamMessageListView( parentMessage: parent, ), ), - MessageInput( + StreamMessageInput( messageInputController: MessageInputController( message: Message(parentId: parent!.id), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index 347ceaf4..fd6081f7 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -8,7 +8,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// the SDK supports easily. /// /// Replacing the built-in message component with your own is done by passing -/// it as a builder function to the [MessageListView] widget. +/// it as a builder function to the [StreamMessageListView] widget. /// /// The message builder function will get the usual [BuildContext] argument /// as well as the [Message] object and its position inside the list. @@ -106,15 +106,15 @@ class ChannelPage extends StatelessWidget { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: [ Expanded( - child: MessageListView( + child: StreamMessageListView( messageBuilder: _messageBuilder, ), ), - const MessageInput(), + const StreamMessageInput(), ], ), ); @@ -124,7 +124,7 @@ class ChannelPage extends StatelessWidget { BuildContext context, MessageDetails details, List messages, - MessageWidget _, + StreamMessageWidget _, ) { final message = details.message; final isCurrentUser = 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 4bb8e928..6ff1bb64 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -58,24 +58,24 @@ class MyApp extends StatelessWidget { final defaultTheme = StreamChatThemeData.fromTheme(themeData); final colorTheme = defaultTheme.colorTheme; final customTheme = defaultTheme.merge(StreamChatThemeData( - channelPreviewTheme: ChannelPreviewThemeData( - avatarTheme: AvatarThemeData( + channelPreviewTheme: StreamChannelPreviewThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(8), ), ), - messageListViewTheme: const MessageListViewThemeData( + messageListViewTheme: const StreamMessageListViewThemeData( backgroundColor: Colors.grey, backgroundImage: DecorationImage( image: AssetImage('assets/background_doodle.png'), fit: BoxFit.cover, ), ), - otherMessageTheme: MessageThemeData( + otherMessageTheme: StreamMessageThemeData( messageBackgroundColor: colorTheme.textHighEmphasis, messageTextStyle: TextStyle( color: colorTheme.barsBg, ), - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(8), ), ), @@ -137,17 +137,17 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( - appBar: const ChannelHeader(), + appBar: const StreamChannelHeader(), body: Column( children: [ Expanded( - child: MessageListView( + child: StreamMessageListView( threadBuilder: (_, parentMessage) => ThreadPage( parent: parentMessage, ), ), ), - const MessageInput(), + const StreamMessageInput(), ], ), ); @@ -165,17 +165,17 @@ class ThreadPage extends StatelessWidget { // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { return Scaffold( - appBar: ThreadHeader( + appBar: StreamThreadHeader( parent: parent!, ), body: Column( children: [ Expanded( - child: MessageListView( + child: StreamMessageListView( parentMessage: parent, ), ), - MessageInput( + StreamMessageInput( messageInputController: MessageInputController( message: Message(parentId: parent!.id), ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index f1369d49..b1aba4a2 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -1,17 +1,23 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro attachment_title} +@Deprecated("Use 'StreamAttachmentTitle' instead") +typedef AttachmentTitle = StreamAttachmentTitle; + +/// {@template attachment_title} /// Title for attachments -class AttachmentTitle extends StatelessWidget { +/// {@endtemplate} +class StreamAttachmentTitle extends StatelessWidget { /// Supply attachment and theme for constructing title - const AttachmentTitle({ + const StreamAttachmentTitle({ Key? key, required this.attachment, required this.messageTheme, }) : super(key: key); /// Theme to apply to text - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; /// Attachment data to display final Attachment attachment; diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index bd0aacc3..a1aa60bc 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart @@ -9,10 +9,16 @@ typedef InProgressBuilder = Widget Function(BuildContext, int, int); /// Widget to build on failure typedef FailedBuilder = Widget Function(BuildContext, String); +/// {@macro attachment_upload_state_builder} +@Deprecated("Use 'StreamAttachmentsUploadStateBuilder' instead") +typedef AttachmentUploadStateBuilder = StreamAttachmentUploadStateBuilder; + +/// {@template attachment_upload_state_builder} /// Widget to display attachment upload state -class AttachmentUploadStateBuilder extends StatelessWidget { - /// Constructor for creating an [AttachmentUploadStateBuilder] widget - const AttachmentUploadStateBuilder({ +/// {@endtemplate} +class StreamAttachmentUploadStateBuilder extends StatelessWidget { + /// Constructor for creating an [StreamAttachmentUploadStateBuilder] widget + const StreamAttachmentUploadStateBuilder({ Key? key, required this.message, required this.attachment, @@ -137,7 +143,7 @@ class _PreparingState extends StatelessWidget { ), Align( alignment: Alignment.topRight, - child: UploadProgressIndicator( + child: StreamUploadProgressIndicator( uploaded: 0, total: double.maxFinite.toInt(), ), @@ -177,7 +183,7 @@ class _InProgressState extends StatelessWidget { ), Align( alignment: Alignment.topRight, - child: UploadProgressIndicator( + child: StreamUploadProgressIndicator( uploaded: sent, total: total, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart index 75a890e8..0021fc76 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart @@ -28,10 +28,16 @@ extension AttachmentSourceX on AttachmentSource { } } +/// {@macro attachment_widget} +@Deprecated("Use 'StreamAttachmentWidget' instead") +typedef AttachmentWidget = StreamAttachmentWidget; + +/// {@template attachment_widget} /// Abstract class for deriving attachment types -abstract class AttachmentWidget extends StatelessWidget { +/// {@endtemplate} +abstract class StreamAttachmentWidget extends StatelessWidget { /// Constructor for creating attachment widget - const AttachmentWidget({ + const StreamAttachmentWidget({ Key? key, required this.message, required this.attachment, diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 0b5d580f..6bac62e9 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -10,10 +10,16 @@ import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +/// {@macro file_attachment} +@Deprecated("Use 'StreamFileAttachment' instead") +typedef FileAttachment = StreamFileAttachment; + +/// {@template file_attachment} /// Widget for displaying file attachments -class FileAttachment extends AttachmentWidget { +/// {@endtemplate} +class StreamFileAttachment extends StreamAttachmentWidget { /// Constructor for creating a widget when attachment is of type 'file' - const FileAttachment({ + const StreamFileAttachment({ Key? key, required Message message, required Attachment attachment, @@ -157,7 +163,7 @@ class FileAttachment extends AttachmentWidget { type: MaterialType.transparency, shape: _getDefaultShape(context), child: source.when( - local: () => VideoThumbnailImage( + local: () => StreamVideoThumbnailImage( fit: BoxFit.cover, video: attachment.file!.path!, placeholderBuilder: (_) => const Center( @@ -168,7 +174,7 @@ class FileAttachment extends AttachmentWidget { ), ), ), - network: () => VideoThumbnailImage( + network: () => StreamVideoThumbnailImage( fit: BoxFit.cover, video: attachment.assetUrl!, placeholderBuilder: (_) => const Center( @@ -278,7 +284,7 @@ class FileAttachment extends AttachmentWidget { ); return attachment.uploadState.when( preparing: () => Text(fileSize(size), style: textStyle), - inProgress: (sent, total) => UploadProgressIndicator( + inProgress: (sent, total) => StreamUploadProgressIndicator( uploaded: sent, total: total, showBackground: false, 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 1974b040..28e14e5e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -5,10 +5,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro giphy_attachment} +@Deprecated("Use 'StreamGiphyAttachment' instead") +typedef GiphyAttachment = StreamGiphyAttachment; + +/// {@template giphy_attachment} /// Widget for showing a GIF attachment -class GiphyAttachment extends AttachmentWidget { - /// Constructor for creating a [GiphyAttachment] widget - const GiphyAttachment({ +/// {@endtemplate} +class StreamGiphyAttachment extends StreamAttachmentWidget { + /// Constructor for creating a [StreamGiphyAttachment] widget + const StreamGiphyAttachment({ Key? key, required Message message, required Attachment attachment, @@ -228,7 +234,7 @@ class GiphyAttachment extends AttachmentWidget { alignment: Alignment.centerRight, child: Padding( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: VisibleFootnote(), + child: StreamVisibleFootnote(), ), ), ], @@ -243,7 +249,7 @@ class GiphyAttachment extends AttachmentWidget { final channel = StreamChannel.of(context).channel; return StreamChannel( channel: channel, - child: FullScreenMedia( + child: StreamFullScreenMedia( mediaAttachments: message.attachments, startIndex: message.attachments.indexOf(attachment), userName: message.user?.name, 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 a46ff25c..b6de6bc1 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -5,10 +5,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro image_attachment} +@Deprecated("use 'StreamImageAttachment' instead") +typedef ImageAttachment = StreamImageAttachment; + +/// {@template image_attachment} /// Widget for showing an image attachment -class ImageAttachment extends AttachmentWidget { - /// Constructor for creating a [ImageAttachment] widget - const ImageAttachment({ +/// {@endtemplate} +class StreamImageAttachment extends StreamAttachmentWidget { + /// Constructor for creating a [StreamImageAttachment] widget + const StreamImageAttachment({ Key? key, required Message message, required Attachment attachment, @@ -25,8 +31,8 @@ class ImageAttachment extends AttachmentWidget { size: size, ); - /// [MessageThemeData] for showing image title - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] for showing image title + final StreamMessageThemeData messageTheme; /// Flag for showing title final bool showTitle; @@ -137,7 +143,7 @@ class ImageAttachment extends AttachmentWidget { StreamChannel.of(context).channel; return StreamChannel( channel: channel, - child: FullScreenMedia( + child: StreamFullScreenMedia( mediaAttachments: message.attachments, startIndex: message.attachments.indexOf(attachment), @@ -155,7 +161,7 @@ class ImageAttachment extends AttachmentWidget { ), Padding( padding: const EdgeInsets.all(8), - child: AttachmentUploadStateBuilder( + child: StreamAttachmentUploadStateBuilder( message: message, attachment: attachment, ), @@ -166,7 +172,7 @@ class ImageAttachment extends AttachmentWidget { if (showTitle && attachment.title != null) Material( color: messageTheme.messageBackgroundColor, - child: AttachmentTitle( + child: StreamAttachmentTitle( messageTheme: messageTheme, attachment: attachment, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart index 8a79ca21..160b87da 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -2,10 +2,16 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro url_attachment} +@Deprecated("Use 'StreamUrlAttachment' instead") +typedef UrlAttachment = StreamUrlAttachment; + +/// {@template url_attachment} /// Widget to display URL attachment -class UrlAttachment extends StatelessWidget { - /// Constructor for creating a [UrlAttachment] - const UrlAttachment({ +/// {@endtemplate} +class StreamUrlAttachment extends StatelessWidget { + /// Constructor for creating a [StreamUrlAttachment] + const StreamUrlAttachment({ Key? key, required this.urlAttachment, required this.hostDisplayName, @@ -25,8 +31,8 @@ class UrlAttachment extends StatelessWidget { /// Padding for text final EdgeInsets textPadding; - /// [MessageThemeData] for showing image title - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] for showing image title + final StreamMessageThemeData messageTheme; @override Widget build(BuildContext context) { 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 3df1f451..541f1484 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -4,10 +4,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro video_attachment} +@Deprecated("Use 'StreamVideoAttachment' instead") +typedef VideoAttachment = StreamVideoAttachment; + +/// {@template video_attachment} /// Widget for showing a video attachment -class VideoAttachment extends AttachmentWidget { - /// Constructor for creating a [VideoAttachment] widget - const VideoAttachment({ +/// {@endtemplate} +class StreamVideoAttachment extends StreamAttachmentWidget { + /// Constructor for creating a [StreamVideoAttachment] widget + const StreamVideoAttachment({ Key? key, required Message message, required Attachment attachment, @@ -23,8 +29,8 @@ class VideoAttachment extends AttachmentWidget { size: size, ); - /// [MessageThemeData] for showing title - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] for showing title + final StreamMessageThemeData messageTheme; /// Callback when show message is tapped final ShowMessageCallback? onShowMessage; @@ -43,7 +49,7 @@ class VideoAttachment extends AttachmentWidget { } return _buildVideoAttachment( context, - VideoThumbnailImage( + StreamVideoThumbnailImage( video: attachment.file!.path!, height: size?.height, width: size?.width, @@ -58,7 +64,7 @@ class VideoAttachment extends AttachmentWidget { } return _buildVideoAttachment( context, - VideoThumbnailImage( + StreamVideoThumbnailImage( video: attachment.assetUrl!, height: size?.height, width: size?.width, @@ -84,7 +90,7 @@ class VideoAttachment extends AttachmentWidget { MaterialPageRoute( builder: (_) => StreamChannel( channel: channel, - child: FullScreenMedia( + child: StreamFullScreenMedia( mediaAttachments: message.attachments, startIndex: message.attachments.indexOf(attachment), @@ -111,7 +117,7 @@ class VideoAttachment extends AttachmentWidget { ), Padding( padding: const EdgeInsets.all(8), - child: AttachmentUploadStateBuilder( + child: StreamAttachmentUploadStateBuilder( message: message, attachment: attachment, ), @@ -123,7 +129,7 @@ class VideoAttachment extends AttachmentWidget { if (attachment.title != null) Material( color: messageTheme.messageBackgroundColor, - child: AttachmentTitle( + child: StreamAttachmentTitle( messageTheme: messageTheme, attachment: attachment, ), 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 4fedfc12..41e6fd11 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -58,7 +58,7 @@ class AttachmentActionsModal extends StatelessWidget { /// List of custom actions final List customActions; - /// Creates a copy of [MessageWidget] with specified attributes overridden. + /// Creates a copy of [StreamMessageWidget] with specified attributes overridden. AttachmentActionsModal copyWith({ Key? key, int? currentIndex, diff --git a/packages/stream_chat_flutter/lib/src/back_button.dart b/packages/stream_chat_flutter/lib/src/back_button.dart index 08d8dabf..b7cd4552 100644 --- a/packages/stream_chat_flutter/lib/src/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/back_button.dart @@ -50,7 +50,7 @@ class StreamBackButton extends StatelessWidget { Positioned( top: 7, right: 7, - child: UnreadIndicator( + child: StreamUnreadIndicator( cid: cid, ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index f7c949ee..077302dd 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -152,7 +152,7 @@ class ChannelAvatar extends StatelessWidget { return BetterStreamBuilder( stream: streamChat.client.state.currentUserStream.map((it) => it!), initialData: currentUser, - builder: (context, user) => UserAvatar( + builder: (context, user) => StreamUserAvatar( borderRadius: borderRadius ?? previewTheme?.borderRadius, user: user, constraints: constraints ?? previewTheme?.constraints, @@ -175,7 +175,7 @@ class ChannelAvatar extends StatelessWidget { ), ), initialData: member, - builder: (context, member) => UserAvatar( + builder: (context, member) => StreamUserAvatar( borderRadius: borderRadius ?? previewTheme?.borderRadius, user: member.user!, constraints: constraints ?? previewTheme?.constraints, @@ -188,7 +188,7 @@ class ChannelAvatar extends StatelessWidget { } // Group conversation - return GroupAvatar( + return StreamGroupAvatar( members: otherMembers, borderRadius: borderRadius ?? previewTheme?.borderRadius, constraints: constraints ?? previewTheme?.constraints, diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 05d5996e..66438479 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -4,10 +4,7 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Bottom Sheet with options -@Deprecated( - "'ChannelBottomSheet' is deprecated and shouldn't be used. " - "Please use 'StreamChannelBottomSheet' instead.", -) +@Deprecated("Use 'StreamChannelInfoBottomSheet' instead") class ChannelBottomSheet extends StatefulWidget { /// Constructor for creating bottom sheet const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key); @@ -24,7 +21,7 @@ class _ChannelBottomSheetState extends State { bool _showActions = true; late StreamChannelState _streamChannelState; - late ChannelPreviewThemeData _channelPreviewThemeData; + late StreamChannelPreviewThemeData _channelPreviewThemeData; late StreamChatThemeData _streamChatThemeData; late StreamChatState _streamChatState; @@ -58,7 +55,8 @@ class _ChannelBottomSheetState extends State { Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: ChannelName( + child: StreamChannelName( + channel: channel, textStyle: _streamChatThemeData.textTheme.headlineBold, ), ), @@ -67,7 +65,7 @@ class _ChannelBottomSheetState extends State { height: 5, ), Center( - child: ChannelInfo( + child: StreamChannelInfo( showTypingIndicator: false, channel: _streamChannelState.channel, textStyle: _channelPreviewThemeData.subtitleStyle, @@ -79,7 +77,7 @@ class _ChannelBottomSheetState extends State { if (channel.isDistinct && channel.memberCount == 2) Column( children: [ - UserAvatar( + StreamUserAvatar( user: members .firstWhere( (e) => e.user?.id != userAsMember.user?.id, @@ -122,7 +120,7 @@ class _ChannelBottomSheetState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( children: [ - UserAvatar( + StreamUserAvatar( user: members[index].user!, constraints: const BoxConstraints.tightFor( height: 64, @@ -150,7 +148,7 @@ class _ChannelBottomSheetState extends State { const SizedBox( height: 24, ), - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.user( @@ -163,7 +161,7 @@ class _ChannelBottomSheetState extends State { if (!channel.isDistinct && channel.ownCapabilities .contains(PermissionType.leaveChannel)) - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.userRemove( @@ -184,7 +182,7 @@ class _ChannelBottomSheetState extends State { if (isOwner && channel.ownCapabilities .contains(PermissionType.deleteChannel)) - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.delete( @@ -203,7 +201,7 @@ class _ChannelBottomSheetState extends State { }); }, ), - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.closeSmall( @@ -224,7 +222,7 @@ class _ChannelBottomSheetState extends State { void didChangeDependencies() { _streamChannelState = StreamChannel.of(context); _streamChatThemeData = StreamChatTheme.of(context); - _channelPreviewThemeData = ChannelPreviewTheme.of(context); + _channelPreviewThemeData = StreamChannelPreviewTheme.of(context); _streamChatState = StreamChat.of(context); super.didChangeDependencies(); } diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 954aa758..9da8b4f8 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -4,6 +4,11 @@ import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +///{@macro template_name} +@Deprecated("Use 'StreamChannelHeader' instead") +typedef ChannelHeader = StreamChannelHeader; + +/// {@template channel_header} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png) /// @@ -49,9 +54,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The widget components render the ui based on the first ancestor of type /// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. /// Modify it to change the widget appearance. -class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { +/// {@endtemplate} +class StreamChannelHeader extends StatelessWidget + implements PreferredSizeWidget { /// Creates a channel header - const ChannelHeader({ + const StreamChannelHeader({ Key? key, this.showBackButton = true, this.onBackPressed, @@ -96,16 +103,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { final Widget? leading; /// AppBar actions - /// By default it shows the [ChannelAvatar] + /// By default it shows the [StreamChannelAvatar] final List? actions; - /// The background color for this [ChannelHeader]. + /// The background color for this [StreamChannelHeader]. final Color? backgroundColor; @override Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; - final channelHeaderTheme = ChannelHeaderTheme.of(context); + final channelHeaderTheme = StreamChannelHeaderTheme.of(context); final leadingWidget = leading ?? (showBackButton @@ -115,7 +122,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { ) : const SizedBox()); - return ConnectionStatusBuilder( + return StreamConnectionStatusBuilder( statusBuilder: (context, status) { var statusString = ''; var showStatus = true; @@ -135,7 +142,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { final theme = Theme.of(context); - return InfoTile( + return StreamInfoTile( showMessage: showConnectionStateTile && showStatus, message: statusString, child: AppBar( @@ -152,7 +159,8 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { Padding( padding: const EdgeInsets.only(right: 10), child: Center( - child: ChannelAvatar( + child: StreamChannelAvatar( + channel: channel, borderRadius: channelHeaderTheme.avatarTheme?.borderRadius, constraints: @@ -172,12 +180,13 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ title ?? - ChannelName( + StreamChannelName( + channel: channel, textStyle: channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), subtitle ?? - ChannelInfo( + StreamChannelInfo( showTypingIndicator: showTypingIndicator, channel: channel, textStyle: channelHeaderTheme.subtitleStyle, diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 6a8e0634..b30058b9 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -3,10 +3,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro channel_info} +@Deprecated("Use 'StreamChannelInfo' instead") +typedef ChannelInfo = StreamChannelInfo; + +/// {@template channel_info} /// Widget which shows channel info -class ChannelInfo extends StatelessWidget { - /// Constructor which creates a [ChannelInfo] widget - const ChannelInfo({ +/// {@endtemplate} +class StreamChannelInfo extends StatelessWidget { + /// Constructor which creates a [StreamChannelInfo] widget + const StreamChannelInfo({ Key? key, required this.channel, this.textStyle, @@ -32,7 +38,7 @@ class ChannelInfo extends StatelessWidget { return BetterStreamBuilder>( stream: channel.state!.membersStream, initialData: channel.state!.members, - builder: (context, data) => ConnectionStatusBuilder( + builder: (context, data) => StreamConnectionStatusBuilder( statusBuilder: (context, status) { switch (status) { case ConnectionStatus.connected: @@ -66,7 +72,7 @@ class ChannelInfo extends StatelessWidget { } alternativeWidget = Text( text, - style: ChannelHeaderTheme.of(context).subtitleStyle, + style: StreamChannelHeaderTheme.of(context).subtitleStyle, ); } else { final userId = StreamChat.of(context).currentUser?.id; @@ -95,7 +101,7 @@ class ChannelInfo extends StatelessWidget { } return Align( - child: TypingIndicator( + child: StreamTypingIndicator( parentId: parentId, style: textStyle, alternativeWidget: alternativeWidget, diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 6c54ad89..f814a08e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -11,7 +11,11 @@ typedef TitleBuilder = Widget Function( StreamChatClient client, ); -/// +/// {@macro channel_list_header} +@Deprecated("Use 'StreamChannelListHeader' instead") +typedef ChannelListHeader = StreamChannelListHeader; + +/// {@template channel_list_header} /// It shows the current [StreamChatClient] status. /// /// ```dart @@ -43,11 +47,13 @@ typedef TitleBuilder = Widget Function( /// if you don't have it in the widget tree. /// /// The widget components render the ui based on the first ancestor of type -/// [StreamChatTheme] and on its [ChannelListHeaderThemeData] property. +/// [StreamChatTheme] and on its [StreamChannelListHeaderThemeData] property. /// Modify it to change the widget appearance. -class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { +/// {@endtemplate} +class StreamChannelListHeader extends StatelessWidget + implements PreferredSizeWidget { /// Instantiates a ChannelListHeader - const ChannelListHeader({ + const StreamChannelListHeader({ Key? key, this.client, this.titleBuilder, @@ -91,14 +97,14 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it shows the new chat button final List? actions; - /// The background color for this [ChannelListHeader]. + /// The background color for this [StreamChannelListHeader]. final Color? backgroundColor; @override Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; final user = _client.state.currentUser; - return ConnectionStatusBuilder( + return StreamConnectionStatusBuilder( statusBuilder: (context, status) { var statusString = ''; var showStatus = true; @@ -117,9 +123,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { } final chatThemeData = StreamChatTheme.of(context); - final channelListHeaderThemeData = ChannelListHeaderTheme.of(context); + final channelListHeaderThemeData = + StreamChannelListHeaderTheme.of(context); final theme = Theme.of(context); - return InfoTile( + return StreamInfoTile( showMessage: showConnectionStateTile && showStatus, message: statusString, child: AppBar( @@ -135,7 +142,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { leading: leading ?? Center( child: user != null - ? UserAvatar( + ? StreamUserAvatar( user: user, showOnlineStatus: false, onTap: onUserAvatarTap ?? @@ -156,7 +163,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { [ StreamNeumorphicButton( child: IconButton( - icon: ConnectionStatusBuilder( + icon: StreamConnectionStatusBuilder( statusBuilder: (context, status) { Color? color; switch (status) { @@ -234,10 +241,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { const SizedBox(width: 10), Text( context.translations.searchingForNetworkText, - style: ChannelListHeaderTheme.of(context).titleStyle?.copyWith( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: + StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), ], ); @@ -247,7 +255,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { StreamChatClient client, ) { final chatThemeData = StreamChatTheme.of(context); - final channelListHeaderTheme = ChannelListHeaderTheme.of(context); + final channelListHeaderTheme = StreamChannelListHeaderTheme.of(context); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index cb581491..1113fda2 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -11,7 +10,7 @@ typedef ChannelTapCallback = void Function(Channel, Widget?); /// Callback called when tapping on a channel typedef ChannelInfoCallback = void Function(Channel); -/// Builder used to create a custom [ChannelPreview] from a [Channel] +/// Builder used to create a custom [StreamChannelPreview] from a [Channel] typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel); /// Callback for when 'View Info' is tapped @@ -52,10 +51,7 @@ typedef ViewInfoCallback = void Function(Channel); /// The widget components render the ui based on the first ancestor of /// type [StreamChatTheme]. /// Modify it to change the widget appearance. -@Deprecated( - "'ChannelListView' is deprecated and shouldn't be used. " - "Please use 'StreamChannelListView' instead.", -) +@Deprecated("Use 'StreamChannelListView' instead") class ChannelListView extends StatefulWidget { /// Instantiate a new ChannelListView ChannelListView({ @@ -248,7 +244,8 @@ class _ChannelListViewState extends State { child: child, ); - final backgroundColor = ChannelListViewTheme.of(context).backgroundColor; + final backgroundColor = + StreamChannelListViewTheme.of(context).backgroundColor; if (backgroundColor != null) { return ColoredBox( @@ -550,7 +547,8 @@ class _ChannelListViewState extends State { context: context, builder: (context) => StreamChannel( channel: channel, - child: ChannelBottomSheet( + child: StreamChannelInfoBottomSheet( + channel: channel, onViewInfoTap: () { widget.onViewInfoTap?.call(channel); }, @@ -593,7 +591,7 @@ class _ChannelListViewState extends State { decoration: BoxDecoration( color: chatThemeData.channelListViewTheme.backgroundColor, ), - child: ChannelPreview( + child: StreamChannelPreview( onLongPress: widget.onChannelLongPress, channel: channel, onImageTap: widget.onImageTap != null @@ -639,7 +637,7 @@ class _ChannelListViewState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ChannelAvatar( + StreamChannelAvatar( channel: channel, borderRadius: BorderRadius.circular(32), selected: selected, @@ -654,8 +652,9 @@ class _ChannelListViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 8), child: StreamChannel( channel: channel, - child: const ChannelName( - textStyle: TextStyle( + child: StreamChannelName( + channel: channel, + textStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ), diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 8dd8c054..f496b28a 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -6,10 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// /// The widget uses a [StreamBuilder] to render the channel information /// image as soon as it updates. -@Deprecated( - "'ChannelName' is deprecated and shouldn't be used. " - "Please use 'StreamChannelName' instead.", -) +@Deprecated("Use 'StreamChannelName' instead") class ChannelName extends StatelessWidget { /// Instantiate a new ChannelName const ChannelName({ diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 125d06fe..1eb28bd2 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -4,6 +4,11 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro channel_preview} +@Deprecated("Use 'StreamChannelPreview' instead") +typedef ChannelPreview = StreamChannelPreview; + +/// {@template channel_preview} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png) /// @@ -13,14 +18,15 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// image as soon as it updates. /// /// Usually you don't use this widget as it's the default channel preview -/// used by [ChannelListView]. +/// used by [StreamChannelListView]. /// /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. -class ChannelPreview extends StatelessWidget { - /// Constructor for creating [ChannelPreview] - const ChannelPreview({ +/// {@endtemplate} +class StreamChannelPreview extends StatelessWidget { + /// Constructor for creating [StreamChannelPreview] + const StreamChannelPreview({ required this.channel, Key? key, this.onTap, @@ -52,7 +58,7 @@ class ChannelPreview extends StatelessWidget { final Widget? subtitle; /// Widget rendering the leading element, by default - /// it shows the [ChannelAvatar] + /// it shows the [StreamChannelAvatar] final Widget? leading; /// Widget rendering the trailing element, @@ -60,12 +66,12 @@ class ChannelPreview extends StatelessWidget { final Widget? trailing; /// Widget rendering the sending indicator, - /// by default it uses the [SendingIndicator] widget + /// by default it uses the [StreamSendingIndicator] widget final Widget? sendingIndicator; @override Widget build(BuildContext context) { - final channelPreviewTheme = ChannelPreviewTheme.of(context); + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); final streamChatState = StreamChat.of(context); return BetterStreamBuilder( stream: channel.isMutedStream, @@ -80,13 +86,18 @@ class ChannelPreview extends StatelessWidget { ), onTap: () => onTap?.call(channel), onLongPress: () => onLongPress?.call(channel), - leading: leading ?? ChannelAvatar(onTap: onImageTap), + leading: leading ?? + StreamChannelAvatar( + channel: channel, + onTap: onImageTap, + ), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: title ?? - ChannelName( + StreamChannelName( + channel: channel, textStyle: channelPreviewTheme.titleStyle, ), ), @@ -100,7 +111,7 @@ class ChannelPreview extends StatelessWidget { e.user!.id == channel.client.state.currentUser?.id)) { return const SizedBox(); } - return UnreadIndicator( + return StreamUnreadIndicator( cid: channel.cid, ); }, @@ -136,7 +147,7 @@ class ChannelPreview extends StatelessWidget { ))); final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1; - return SendingIndicator( + return StreamSendingIndicator( message: lastMessage!, size: channelPreviewTheme.indicatorIconSize, isMessageRead: isMessageRead, @@ -183,13 +194,13 @@ class ChannelPreview extends StatelessWidget { return Text( stringDate, - style: ChannelPreviewTheme.of(context).lastMessageAtStyle, + style: StreamChannelPreviewTheme.of(context).lastMessageAtStyle, ); }, ); Widget _buildSubtitle(BuildContext context) { - final channelPreviewTheme = ChannelPreviewTheme.of(context); + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); if (channel.isMuted) { return Row( crossAxisAlignment: CrossAxisAlignment.end, @@ -204,7 +215,7 @@ class ChannelPreview extends StatelessWidget { ], ); } - return TypingIndicator( + return StreamTypingIndicator( channel: channel, alternativeWidget: _buildLastMessage(context), style: channelPreviewTheme.subtitleStyle, @@ -242,7 +253,7 @@ class ChannelPreview extends StatelessWidget { text = parts.join(' '); - final channelPreviewTheme = ChannelPreviewTheme.of(context); + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); return Text.rich( _getDisplayText( text, diff --git a/packages/stream_chat_flutter/lib/src/commands_overlay.dart b/packages/stream_chat_flutter/lib/src/commands_overlay.dart index 9f8aca1e..6234f696 100644 --- a/packages/stream_chat_flutter/lib/src/commands_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/commands_overlay.dart @@ -2,10 +2,17 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro commands_overlay} +@Deprecated("Use 'StreamCommandsOverlay' instead") +typedef CommandsOverlay = StreamCommandsOverlay; + +/// {@template commands_overlay} /// Overlay for displaying commands that can be used -class CommandsOverlay extends StatelessWidget { - /// Constructor for creating a [CommandsOverlay] - const CommandsOverlay({ +/// to interact with the channel. +/// {@endtemplate} +class StreamCommandsOverlay extends StatelessWidget { + /// Constructor for creating a [StreamCommandsOverlay] + const StreamCommandsOverlay({ required this.text, required this.onCommandResult, required this.size, diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index f0eca9da..6f234547 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro connection_status_builder} +@Deprecated("Use 'StreamConnectionStatusBuilder' instead") +typedef ConnectionStatusBuilder = StreamConnectionStatusBuilder; + +/// {@template connection_status_builder} /// Widget that builds itself based on the latest snapshot of interaction with /// a [Stream] of type [ConnectionStatus]. /// /// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] /// in case no stream is provided. -class ConnectionStatusBuilder extends StatelessWidget { +/// {@endtemplate} +class StreamConnectionStatusBuilder extends StatelessWidget { /// Creates a new ConnectionStatusBuilder - const ConnectionStatusBuilder({ + const StreamConnectionStatusBuilder({ Key? key, required this.statusBuilder, this.connectionStatusStream, diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/date_divider.dart index c3b48158..64396ceb 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/date_divider.dart @@ -3,10 +3,16 @@ import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro date_divider} +@Deprecated("Use 'StreamDateDivider' instead") +typedef DateDivider = StreamDateDivider; + +/// {@template date_divider} /// It shows a date divider depending on the date difference -class DateDivider extends StatelessWidget { - /// Constructor for creating a [DateDivider] - const DateDivider({ +/// {@endtemplate} +class StreamDateDivider extends StatelessWidget { + /// Constructor for creating a [StreamDateDivider] + const StreamDateDivider({ Key? key, required this.dateTime, this.uppercase = false, diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index 09d725bb..667d101d 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -3,10 +3,16 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; -/// Widget to display deleted message -class DeletedMessage extends StatelessWidget { - /// Constructor to create [DeletedMessage] - const DeletedMessage({ +/// {@macro deleted_message} +@Deprecated("Use 'StreamDeletedMessage' instead") +typedef DeletedMessage = StreamDeletedMessage; + +/// {@template deleted_message} +/// Widget to display deleted message. +/// {@endtemplate} +class StreamDeletedMessage extends StatelessWidget { + /// Constructor to create [StreamDeletedMessage] + const StreamDeletedMessage({ Key? key, required this.messageTheme, this.borderRadiusGeometry, @@ -16,7 +22,7 @@ class DeletedMessage extends StatelessWidget { }) : super(key: key); /// The theme of the message - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; /// The border radius of the message text final BorderRadiusGeometry? borderRadiusGeometry; diff --git a/packages/stream_chat_flutter/lib/src/emoji_overlay.dart b/packages/stream_chat_flutter/lib/src/emoji_overlay.dart index 0a3cb634..b4e50974 100644 --- a/packages/stream_chat_flutter/lib/src/emoji_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/emoji_overlay.dart @@ -4,10 +4,16 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:substring_highlight/substring_highlight.dart'; +/// {@macro emoji_overlay} +@Deprecated("Use 'StreamEmojiOverlay' instead") +typedef EmojiOverlay = StreamEmojiOverlay; + +/// {@template emoji_overlay} /// Overlay for displaying emoji that can be used -class EmojiOverlay extends StatelessWidget { - /// Constructor for creating a [EmojiOverlay] - const EmojiOverlay({ +/// {@endtemplate} +class StreamEmojiOverlay extends StatelessWidget { + /// Constructor for creating a [StreamEmojiOverlay] + const StreamEmojiOverlay({ required this.query, required this.onEmojiResult, required this.size, 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 aa185597..0ae3a475 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -21,10 +21,16 @@ enum ReturnActionType { /// Callback when show message is tapped typedef ShowMessageCallback = void Function(Message message, Channel channel); +/// {@macro full_screen_media} +@Deprecated("Use 'StreamFullScreenMedia' instead") +typedef FullScreenMedia = StreamFullScreenMedia; + +/// {@template full_screen_media} /// A full screen image widget -class FullScreenMedia extends StatefulWidget { +/// {@endtemplate} +class StreamFullScreenMedia extends StatefulWidget { /// Instantiate a new FullScreenImage - const FullScreenMedia({ + const StreamFullScreenMedia({ Key? key, required this.mediaAttachments, required this.message, @@ -60,10 +66,10 @@ class FullScreenMedia extends StatefulWidget { final bool autoplayVideos; @override - _FullScreenMediaState createState() => _FullScreenMediaState(); + _StreamFullScreenMediaState createState() => _StreamFullScreenMediaState(); } -class _FullScreenMediaState extends State +class _StreamFullScreenMediaState extends State with SingleTickerProviderStateMixin { late final AnimationController _animationController; late final PageController _pageController; @@ -172,7 +178,7 @@ class _FullScreenMediaState extends State ), backgroundDecoration: BoxDecoration( color: ColorTween( - begin: ChannelHeaderTheme.of(context).color, + begin: StreamChannelHeaderTheme.of(context).color, end: Colors.black, ).lerp(_curvedAnimation.value), ), @@ -221,7 +227,7 @@ class _FullScreenMediaState extends State builder: (context, value, child) => Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - GalleryHeader( + StreamGalleryHeader( userName: widget.userName, sentAt: context.translations.sentAtText( date: widget.message.createdAt, @@ -242,7 +248,7 @@ class _FullScreenMediaState extends State widget.attachmentActionsModalBuilder, ), if (!widget.message.isEphemeral) - GalleryFooter( + StreamGalleryFooter( currentPage: value, totalPages: widget.mediaAttachments.length, mediaAttachments: widget.mediaAttachments, diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index f189dae3..a137281c 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -9,10 +9,17 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro gallery_footer} +@Deprecated("Use 'StreamGalleryFooter' instead") +typedef GalleryFooter = StreamGalleryFooter; + +/// {@template gallery_footer} /// Footer widget for media display -class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { +/// {@endtemplate} +class StreamGalleryFooter extends StatefulWidget + implements PreferredSizeWidget { /// Creates a channel header - const GalleryFooter({ + const StreamGalleryFooter({ Key? key, required this.message, this.onBackPressed, @@ -51,22 +58,22 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { /// Callback when media is selected final ValueChanged? mediaSelectedCallBack; - /// The background color of this [GalleryFooter]. + /// The background color of this [StreamGalleryFooter]. final Color? backgroundColor; @override - _GalleryFooterState createState() => _GalleryFooterState(); + _StreamGalleryFooterState createState() => _StreamGalleryFooterState(); @override final Size preferredSize; } -class _GalleryFooterState extends State { +class _StreamGalleryFooterState extends State { @override Widget build(BuildContext context) { const showShareButton = !kIsWeb; final mediaQueryData = MediaQuery.of(context); - final galleryFooterThemeData = GalleryFooterTheme.of(context); + final galleryFooterThemeData = StreamGalleryFooterTheme.of(context); return SizedBox.fromSize( size: Size( mediaQueryData.size.width, @@ -149,7 +156,7 @@ class _GalleryFooterState extends State { void _showPhotosModal(context) { final chatThemeData = StreamChatTheme.of(context); - final galleryFooterThemeData = GalleryFooterTheme.of(context); + final galleryFooterThemeData = StreamGalleryFooterTheme.of(context); showModalBottomSheet( context: context, barrierColor: galleryFooterThemeData.bottomSheetBarrierColor, @@ -222,7 +229,7 @@ class _GalleryFooterState extends State { onTap: () => widget.mediaSelectedCallBack!(index), child: FittedBox( fit: BoxFit.cover, - child: VideoThumbnailImage( + child: StreamVideoThumbnailImage( video: (attachment.file?.path ?? attachment.assetUrl)!, ), @@ -264,7 +271,7 @@ class _GalleryFooterState extends State { ), ], ), - child: UserAvatar( + child: StreamUserAvatar( user: widget.message.user!, constraints: BoxConstraints.tight(const Size(24, 24)), diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index 2325af69..9c8650b2 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -15,10 +15,17 @@ typedef AttachmentActionsBuilder = Widget Function( AttachmentActionsModal defaultActionsModal, ); +/// {@macro gallery_header} +@Deprecated("Use 'StreamGalleryHeader' instead") +typedef GalleryHeader = StreamGalleryHeader; + +/// {@template gallery_header} /// Header/AppBar widget for media display screen -class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { +/// {@endtemplate} +class StreamGalleryHeader extends StatelessWidget + implements PreferredSizeWidget { /// Creates a channel header - const GalleryHeader({ + const StreamGalleryHeader({ Key? key, required this.message, this.currentIndex = 0, @@ -62,7 +69,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// Stores the current index of media shown final int currentIndex; - /// The background color of this [GalleryHeader]. + /// The background color of this [StreamGalleryHeader]. final Color? backgroundColor; /// Widget builder for attachment actions modal @@ -72,7 +79,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - final galleryHeaderThemeData = GalleryHeaderTheme.of(context); + final galleryHeaderThemeData = StreamGalleryHeaderTheme.of(context); final theme = Theme.of(context); return AppBar( toolbarTextStyle: theme.textTheme.bodyText2, diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart index 12824f45..aca07c92 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -3,10 +3,16 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; +/// {@macro gradient_avatar} +@Deprecated("Use 'StreamGradientAvatar' instead") +typedef GradientAvatar = StreamGradientAvatar; + +/// {@template gradient_avatar} /// Fallback user avatar with a polygon gradient overlayed with text -class GradientAvatar extends StatefulWidget { - /// Constructor for [GradientAvatar] - const GradientAvatar({ +/// {@endtemplate} +class StreamGradientAvatar extends StatefulWidget { + /// Constructor for [StreamGradientAvatar] + const StreamGradientAvatar({ Key? key, required this.name, required this.userId, @@ -19,10 +25,10 @@ class GradientAvatar extends StatefulWidget { final String userId; @override - _GradientAvatarState createState() => _GradientAvatarState(); + _StreamGradientAvatarState createState() => _StreamGradientAvatarState(); } -class _GradientAvatarState extends State { +class _StreamGradientAvatarState extends State { @override Widget build(BuildContext context) => Center( child: RepaintBoundary( diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/group_avatar.dart index e0e87e23..f7efa32d 100644 --- a/packages/stream_chat_flutter/lib/src/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/group_avatar.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro group_avatar} +@Deprecated("Use 'StreamGroupAvatar' instead") +typedef GroupAvatar = StreamGroupAvatar; + +/// {@template group_avatar} /// Widget for constructing a group of images -class GroupAvatar extends StatelessWidget { - /// Constructor for creating a [GroupAvatar] - const GroupAvatar({ +/// {@endtemplate} +class StreamGroupAvatar extends StatelessWidget { + /// Constructor for creating a [StreamGroupAvatar] + const StreamGroupAvatar({ Key? key, this.channel, required this.members, @@ -84,7 +90,7 @@ class GroupAvatar extends StatelessWidget { ), ), initialData: member, - builder: (context, member) => UserAvatar( + builder: (context, member) => StreamUserAvatar( showOnlineStatus: false, user: member.user!, borderRadius: BorderRadius.zero, @@ -122,7 +128,8 @@ class GroupAvatar extends StatelessWidget { ), ), initialData: member, - builder: (context, member) => UserAvatar( + builder: (context, member) => + StreamUserAvatar( showOnlineStatus: false, user: member.user!, borderRadius: BorderRadius.zero, diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index ff67b068..fa723bef 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro image_group} +@Deprecated("Use 'StreamImageGroup' instead") +typedef ImageGroup = StreamImageGroup; + +/// {@template image_group} /// Widget for constructing a group of images in message -class ImageGroup extends StatelessWidget { - /// Constructor for creating [ImageGroup] widget - const ImageGroup({ +/// {@endtemplate} +class StreamImageGroup extends StatelessWidget { + /// Constructor for creating [StreamImageGroup] widget + const StreamImageGroup({ Key? key, required this.images, required this.message, @@ -27,8 +33,8 @@ class ImageGroup extends StatelessWidget { /// Message which images are attached to final Message message; - /// [MessageThemeData] to apply to message - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] to apply to message + final StreamMessageThemeData messageTheme; /// Size of iamges final Size size; @@ -129,7 +135,7 @@ class ImageGroup extends StatelessWidget { MaterialPageRoute( builder: (context) => StreamChannel( channel: channel, - child: FullScreenMedia( + child: StreamFullScreenMedia( mediaAttachments: images, startIndex: index, userName: message.user?.name, @@ -142,7 +148,7 @@ class ImageGroup extends StatelessWidget { if (res != null) onReturnAction?.call(res); } - Widget _buildImage(BuildContext context, int index) => ImageAttachment( + Widget _buildImage(BuildContext context, int index) => StreamImageAttachment( attachment: images[index], size: size, message: message, diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/info_tile.dart index 965d4fed..86964bf0 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/info_tile.dart @@ -2,10 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro info_tile} +@Deprecated("Use 'StreamInfoTile' instead") +typedef InfoTile = StreamInfoTile; + +/// {@template info_tile} /// Tile to display a message, used in stream chat to display connection status -class InfoTile extends StatelessWidget { - /// Constructor for creating an [InfoTile] widget - const InfoTile({ +/// {@endtemplate} +class StreamInfoTile extends StatelessWidget { + /// Constructor for creating an [StreamInfoTile] widget + const StreamInfoTile({ Key? key, required this.message, required this.child, diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index ed15dad8..2a87e76d 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -59,7 +59,7 @@ abstract class Translations { /// The error shown when loading messages fails String get loadingMessagesError; - /// The text for showing the result count in [MessageSearchListView] + /// The text for showing the result count in [StreamMessageSearchListView] String resultCountText(int count); /// The text for showing the message is deleted @@ -74,20 +74,20 @@ abstract class Translations { /// The text for showing there are no chats String get emptyChatMessagesText; - /// The text for showing the thread separator in case [MessageListView] + /// The text for showing the thread separator in case [StreamMessageListView] /// contains a parent message String threadSeparatorText(int replyCount); - /// The label for "connected" in [ConnectionStatusBuilder] + /// The label for "connected" in [StreamConnectionStatusBuilder] String get connectedLabel; - /// The label for "disconnected" in [ConnectionStatusBuilder] + /// The label for "disconnected" in [StreamConnectionStatusBuilder] String get disconnectedLabel; - /// The label for "reconnecting" in [ConnectionStatusBuilder] + /// The label for "reconnecting" in [StreamConnectionStatusBuilder] String get reconnectingLabel; - /// The label for also send as direct message "checkbox"" in [MessageInput] + /// The label for also send as direct message "checkbox"" in [StreamMessageInput] String get alsoSendAsDirectMessageLabel; /// The label for search Gif @@ -97,24 +97,24 @@ abstract class Translations { String get sendMessagePermissionError; /// The label for add a comment or send in case of - /// attachments inside [MessageInput] + /// attachments inside [StreamMessageInput] String get addACommentOrSendLabel; - /// The label for write a message in [MessageInput] + /// The label for write a message in [StreamMessageInput] String get writeAMessageLabel; - /// The label for slow mode enabled in [MessageInput] + /// The label for slow mode enabled in [StreamMessageInput] String get slowModeOnLabel; - /// The label for instant commands in [MessageInput] + /// The label for instant commands in [StreamMessageInput] String get instantCommandsLabel; /// The error shown in case the fi"le is too large even after compression - /// while uploading via [MessageInput] + /// while uploading via [StreamMessageInput] String fileTooLargeAfterCompressionError(double limitInMB); /// The error shown in case the file is too large - /// while uploading via [MessageInput] + /// while uploading via [StreamMessageInput] String fileTooLargeError(double limitInMB); /// The text for showing the query while searching for emojis diff --git a/packages/stream_chat_flutter/lib/src/media_list_view.dart b/packages/stream_chat_flutter/lib/src/media_list_view.dart index d5a9233c..6a3d6cdd 100644 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/media_list_view.dart @@ -7,21 +7,16 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -extension on Duration { - String format() { - final s = '$this'.split('.')[0].padLeft(8, '0'); - if (s.startsWith('00:')) { - return s.replaceFirst('00:', ''); - } - - return s; - } -} +/// {@macro media_list_view} +@Deprecated("Use 'StreamMediaListView' instead") +typedef MediaListView = StreamMediaListView; +/// {@template media_list_view} /// Constructs a list of media -class MediaListView extends StatefulWidget { - /// Constructor for creating a [MediaListView] widget - const MediaListView({ +/// {@endtemplate} +class StreamMediaListView extends StatefulWidget { + /// Constructor for creating a [StreamMediaListView] widget + const StreamMediaListView({ Key? key, this.selectedIds = const [], this.onSelect, @@ -34,10 +29,10 @@ class MediaListView extends StatefulWidget { final void Function(AssetEntity media)? onSelect; @override - _MediaListViewState createState() => _MediaListViewState(); + _StreamMediaListViewState createState() => _StreamMediaListViewState(); } -class _MediaListViewState extends State { +class _StreamMediaListViewState extends State { final _media = []; final ScrollController _scrollController = ScrollController(); int _currentPage = 0; @@ -219,3 +214,14 @@ class MediaThumbnailProvider extends ImageProvider { @override String toString() => '$runtimeType("${media.id}")'; } + +extension on Duration { + String format() { + final s = '$this'.split('.')[0].padLeft(8, '0'); + if (s.startsWith('00:')) { + return s.replaceFirst('00:', ''); + } + + return s; + } +} diff --git a/packages/stream_chat_flutter/lib/src/mention_tile.dart b/packages/stream_chat_flutter/lib/src/mention_tile.dart deleted file mode 100644 index c302ccb8..00000000 --- a/packages/stream_chat_flutter/lib/src/mention_tile.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// This widget is used for showing user tiles for mentions -/// Use [title], [subtitle], [leading], [trailing] for -/// substituting widgets in respective positions -@Deprecated('Use `UserMentionTile` instead. Will be removed in future release') -class MentionTile extends StatelessWidget { - /// Constructor for creating a [MentionTile] widget - const MentionTile( - this.member, { - Key? key, - this.title, - this.subtitle, - this.leading, - this.trailing, - }) : super(key: key); - - /// Member to display in the tile - final Member member; - - /// Widget to display as title - final Widget? title; - - /// Widget to display below [title] - final Widget? subtitle; - - /// Widget at the start of the tile - final Widget? leading; - - /// Widget at the end of tile - final Widget? trailing; - - @override - Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - return SizedBox( - height: 56, - child: Row( - children: [ - const SizedBox( - width: 16, - ), - leading ?? - UserAvatar( - constraints: BoxConstraints.tight( - const Size( - 40, - 40, - ), - ), - user: member.user!, - ), - const SizedBox( - width: 8, - ), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title ?? - Text( - member.user!.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: chatThemeData.textTheme.bodyBold, - ), - const SizedBox( - height: 2, - ), - subtitle ?? - Text( - '@${member.userId}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: chatThemeData.textTheme.footnoteBold.copyWith( - color: chatThemeData.colorTheme.textLowEmphasis, - ), - ), - ], - ), - ), - ), - trailing ?? - Padding( - padding: const EdgeInsets.only( - right: 18, - left: 8, - ), - child: StreamSvgIcon.mentions( - color: chatThemeData.colorTheme.accentPrimary, - ), - ), - ], - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_action.dart b/packages/stream_chat_flutter/lib/src/message_action.dart index 49a57b1e..edd732f5 100644 --- a/packages/stream_chat_flutter/lib/src/message_action.dart +++ b/packages/stream_chat_flutter/lib/src/message_action.dart @@ -1,10 +1,16 @@ import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro message_action} +@Deprecated("Use 'StreamMessageActions' instead") +typedef MessageAction = StreamMessageAction; + +/// {@template message_action} /// Class describing a message action -class MessageAction { - /// returns a new instance of a [MessageAction] - MessageAction({ +/// {@endtemplate} +class StreamMessageAction { + /// returns a new instance of a [StreamMessageAction] + StreamMessageAction({ this.leading, this.title, this.onTap, 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 154472d7..056e84fc 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -3,10 +3,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro message_actions_modal} +@Deprecated("Use 'StreamMessageActionsModal' instead") +typedef MessageActionsModal = StreamMessageActionsModal; + +/// {@template message_actions_modal} /// Constructs a modal with actions for a message -class MessageActionsModal extends StatefulWidget { - /// Constructor for creating a [MessageActionsModal] widget - const MessageActionsModal({ +/// {@endtemplate} +class StreamMessageActionsModal extends StatefulWidget { + /// Constructor for creating a [StreamMessageActionsModal] widget + const StreamMessageActionsModal({ Key? key, required this.message, required this.messageWidget, @@ -43,8 +49,8 @@ class MessageActionsModal extends StatefulWidget { /// Message in focus for actions final Message message; - /// [MessageThemeData] for message - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] for message + final StreamMessageThemeData messageTheme; /// Flag for showing reactions final bool? showReactions; @@ -80,13 +86,14 @@ class MessageActionsModal extends StatefulWidget { final bool reverse; /// List of custom actions - final List customActions; + final List customActions; @override - _MessageActionsModalState createState() => _MessageActionsModalState(); + _StreamMessageActionsModalState createState() => + _StreamMessageActionsModalState(); } -class _MessageActionsModalState extends State { +class _StreamMessageActionsModalState extends State { bool _showActions = true; late List _userPermissions; late bool _isMyMessage; @@ -159,7 +166,7 @@ class _MessageActionsModalState extends State { : -(1.2 - divFactor)), 0, ), - child: ReactionPicker( + child: StreamReactionPicker( message: widget.message, ), ), @@ -268,7 +275,7 @@ class _MessageActionsModalState extends State { InkWell _buildCustomAction( BuildContext context, - MessageAction messageAction, + StreamMessageAction messageAction, ) => InkWell( onTap: () { @@ -589,7 +596,7 @@ class _MessageActionsModalState extends State { elevation: 2, clipBehavior: Clip.hardEdge, isScrollControlled: true, - backgroundColor: MessageInputTheme.of(context).inputBackgroundColor, + backgroundColor: StreamMessageInputTheme.of(context).inputBackgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), @@ -631,7 +638,7 @@ class _MessageActionsModalState extends State { if (widget.editMessageInputBuilder != null) widget.editMessageInputBuilder!(context, widget.message) else - MessageInput( + StreamMessageInput( messageInputController: MessageInputController( message: widget.message, ), diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart new file mode 100644 index 00000000..dc06fd91 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -0,0 +1,2110 @@ +import 'dart:async'; +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/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/multi_overlay.dart'; +import 'package:stream_chat_flutter/src/quoted_message_widget.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:video_compress/video_compress.dart'; + +export 'package:video_compress/video_compress.dart' show VideoQuality; + +/// A callback that can be passed to [MessageInput.onError]. +/// +/// This callback should not throw. +/// +/// It exists merely for error reporting, and should not be used otherwise. +typedef ErrorListener = void Function( + Object error, + StackTrace? stackTrace, +); + +/// A callback that can be passed to [MessageInput.onAttachmentLimitExceed]. +/// +/// This callback should not throw. +/// +/// It exists merely for showing custom error, and should not be used otherwise. +typedef AttachmentLimitExceedListener = void Function( + int limit, + String error, +); + +/// Builder for attachment thumbnails +typedef AttachmentThumbnailBuilder = Widget Function( + BuildContext, + Attachment, +); + +/// Builder function for building a mention tile. +typedef MentionTileBuilder = Widget Function( + BuildContext context, + Member member, +); + +/// Builder function for building a user mention tile. +/// +/// Use [StreamUserMentionTile] for the default implementation. +typedef UserMentionTileBuilder = Widget Function( + BuildContext context, + User user, +); + +/// Widget builder for action button. +/// +/// [defaultActionButton] is the default [IconButton] configuration, +/// use [defaultActionButton.copyWith] to easily customize it. +typedef ActionButtonBuilder = Widget Function( + BuildContext context, + IconButton defaultActionButton, +); + +/// Location for actions on the [MessageInput] +enum ActionsLocation { + /// Align to left + left, + + /// Align to right + right, + + /// Align to left but inside the [TextField] + leftInside, + + /// Align to right but inside the [TextField] + rightInside, +} + +/// Default attachments for widget +enum DefaultAttachmentTypes { + /// Image Attachment + image, + + /// Video Attachment + video, + + /// File Attachment + file, +} + +/// Available locations for the sendMessage button relative to the textField +enum SendButtonLocation { + /// inside the textField + inside, + + /// outside the textField + outside, +} + +const _kMinMediaPickerSize = 360.0; + +const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes + +/// 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 +/// +/// ![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 +/// +/// ```dart +/// class ChannelPage extends StatelessWidget { +/// const ChannelPage({ +/// Key key, +/// }) : super(key: key); +/// +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// appBar: ChannelHeader(), +/// body: Column( +/// children: [ +/// Expanded( +/// child: MessageListView( +/// threadBuilder: (_, parentMessage) { +/// return ThreadPage( +/// parent: parentMessage, +/// ); +/// }, +/// ), +/// ), +/// MessageInput(), +/// ], +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// You usually put this widget in the same page of a [StreamMessageListView] +/// as the bottom widget. +/// +/// The widget renders the ui based on the first ancestor of +/// type [StreamChatTheme]. +/// Modify it to change the widget appearance. +@Deprecated("Use 'StreamMessageInput' instead") +class MessageInput extends StatefulWidget { + /// Instantiate a new MessageInput + const MessageInput({ + Key? key, + this.onMessageSent, + this.preMessageSending, + this.parentMessage, + this.editMessage, + this.maxHeight = 150, + this.keyboardType = TextInputType.multiline, + this.disableAttachments = false, + this.initialMessage, + this.textEditingController, + this.actions = const [], + this.actionsLocation = ActionsLocation.left, + this.attachmentThumbnailBuilders, + this.focusNode, + this.quotedMessage, + this.onQuotedMessageCleared, + this.sendButtonLocation = SendButtonLocation.outside, + this.autofocus = false, + this.hideSendAsDm = false, + this.idleSendButton, + this.activeSendButton, + this.showCommandsButton = true, + @Deprecated('''Use `userMentionsTileBuilder` instead. Will be removed in future release''') + this.mentionsTileBuilder, + this.userMentionsTileBuilder, + this.maxAttachmentSize = _kDefaultMaxAttachmentSize, + this.compressedVideoQuality = VideoQuality.DefaultQuality, + this.compressedVideoFrameRate = 30, + this.onError, + this.attachmentLimit = 10, + this.onAttachmentLimitExceed, + this.attachmentButtonBuilder, + this.commandButtonBuilder, + this.customOverlays = const [], + this.mentionAllAppUsers = false, + this.shouldKeepFocusAfterMessage, + }) : assert( + initialMessage == null || editMessage == null, + "Can't provide both `initialMessage` and `editMessage`", + ), + 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; + + /// 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; + + /// Message to start with + final Message? initialMessage; + + /// Function called after sending the message + final void Function(Message)? onMessageSent; + + /// Function called right before sending the message + /// 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; + + /// The keyboard type assigned to the TextField + final TextInputType keyboardType; + + /// If true the attachments button will not be displayed + final bool disableAttachments; + + /// Use this property to hide/show the commands button + final bool showCommandsButton; + + /// Hide send as dm checkbox + final bool hideSendAsDm; + + /// The text controller of the TextField + final TextEditingController? textEditingController; + + /// List of action widgets + final List actions; + + /// The location of the custom actions + final ActionsLocation actionsLocation; + + /// Map that defines a thumbnail builder for an attachment type + final Map? attachmentThumbnailBuilders; + + /// The focus node associated to the TextField + final FocusNode? focusNode; + + /// + final Message? quotedMessage; + + /// + final VoidCallback? onQuotedMessageCleared; + + /// The location of the send button + final SendButtonLocation sendButtonLocation; + + /// Autofocus property passed to the TextField + final bool autofocus; + + /// Send button widget in an idle state + final Widget? idleSendButton; + + /// Send button widget in an active state + final Widget? activeSendButton; + + /// Customize the tile for the mentions overlay. + final MentionTileBuilder? mentionsTileBuilder; + + /// Customize the tile for the mentions overlay. + final UserMentionTileBuilder? userMentionsTileBuilder; + + /// A callback for error reporting + final ErrorListener? onError; + + /// A limit for the no. of attachments that can be sent with a single message. + final int attachmentLimit; + + /// A callback for when the [attachmentLimit] is exceeded. + /// + /// This will override the default error alert behaviour. + final AttachmentLimitExceedListener? onAttachmentLimitExceed; + + /// Builder for customizing the attachment button. + /// + /// The builder contains the default [IconButton] that can be customized by + /// calling `.copyWith`. + final ActionButtonBuilder? attachmentButtonBuilder; + + /// Builder for customizing the command button. + /// + /// The builder contains the default [IconButton] that can be customized by + /// calling `.copyWith`. + final ActionButtonBuilder? commandButtonBuilder; + + /// When enabled mentions search users across the entire app. + /// + /// Defaults to false. + final bool mentionAllAppUsers; + + /// Defines if the [MessageInput] loses focuses after a message is sent. + /// The default behaviour keeps focus until a command is enabled. + final bool? shouldKeepFocusAfterMessage; + + @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] +class MessageInputState extends State { + final _attachments = {}; + final List _mentionedUsers = []; + + final _imagePicker = ImagePicker(); + late final _focusNode = widget.focusNode ?? FocusNode(); + late final _isInternalFocusNode = widget.focusNode == null; + bool _inputEnabled = true; + bool _commandEnabled = false; + bool _showCommandsOverlay = false; + bool _showMentionsOverlay = false; + + Command? _chosenCommand; + bool _actionsShrunk = false; + bool _sendAsDm = false; + bool _openFilePickerSection = false; + int _filePickerIndex = 0; + + /// The editing controller passed to the input TextField + late final TextEditingController textEditingController = + widget.textEditingController ?? TextEditingController(); + + late StreamChatThemeData _streamChatTheme; + late StreamMessageInputThemeData _messageInputTheme; + + bool get _hasQuotedMessage => widget.quotedMessage != null; + + bool get _messageIsPresent => textEditingController.text.trim().isNotEmpty; + + @override + void initState() { + super.initState(); + if (widget.editMessage != null || widget.initialMessage != null) { + _parseExistingMessage(widget.editMessage ?? widget.initialMessage!); + } + textEditingController.addListener(_onChangedDebounced); + _focusNode.addListener(_focusNodeListener); + } + + void _focusNodeListener() { + if (_focusNode.hasFocus) { + _openFilePickerSection = false; + } + } + + int _timeOut = 0; + Timer? _slowModeTimer; + + void _startSlowMode() { + if (!mounted) { + return; + } + final channel = StreamChannel.of(context).channel; + final cooldownStartedAt = channel.cooldownStartedAt; + if (cooldownStartedAt != null) { + final diff = DateTime.now().difference(cooldownStartedAt).inSeconds; + if (diff < channel.cooldown) { + _timeOut = channel.cooldown - diff; + if (_timeOut > 0) { + _slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_timeOut == 0) { + timer.cancel(); + } else { + if (mounted) { + setState(() => _timeOut -= 1); + } + } + }); + } + } + } + } + + void _stopSlowMode() => _slowModeTimer?.cancel(); + + @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; + }); + } + } + }, + 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 (widget.parentMessage != null && !widget.hideSendAsDm) + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + bottom: 12, + ), + child: _buildDmCheckbox(), + ), + _buildFilePickerSection(), + ], + ), + ), + ), + ); + if (widget.editMessage == null) { + child = Material( + elevation: 8, + child: child, + ); + } + + return StreamMultiOverlay( + childAnchor: Alignment.topCenter, + overlayAnchor: Alignment.bottomCenter, + overlayOptions: [ + OverlayOptions( + visible: _showCommandsOverlay, + widget: _buildCommandsOverlayEntry(), + ), + OverlayOptions( + visible: _focusNode.hasFocus && + textEditingController.text.isNotEmpty && + textEditingController.selection.baseOffset > 0 && + textEditingController.text + .substring( + 0, + textEditingController.selection.baseOffset, + ) + .contains(':'), + widget: _buildEmojiOverlay(), + ), + OverlayOptions( + visible: _showMentionsOverlay, + widget: _buildMentionsOverlayEntry(), + ), + ...widget.customOverlays, + ], + child: child, + ); + } + + Flex _buildTextField(BuildContext context) => Flex( + direction: Axis.horizontal, + children: [ + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.left) + _buildExpandActionsButton(context), + _buildTextInput(context), + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.right) + _buildExpandActionsButton(context), + if (widget.sendButtonLocation == SendButtonLocation.outside) + _animateSendButton(context), + ], + ); + + Widget _buildDmCheckbox() => Row( + children: [ + Container( + height: 16, + width: 16, + foregroundDecoration: BoxDecoration( + border: _sendAsDm + ? null + : Border.all( + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), + width: 2, + ), + borderRadius: BorderRadius.circular(3), + ), + child: Center( + child: Material( + borderRadius: BorderRadius.circular(3), + color: _sendAsDm + ? _streamChatTheme.colorTheme.accentPrimary + : _streamChatTheme.colorTheme.barsBg, + child: InkWell( + onTap: () { + setState(() { + _sendAsDm = !_sendAsDm; + }); + }, + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + crossFadeState: _sendAsDm + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: StreamSvgIcon.check( + size: 16, + color: _streamChatTheme.colorTheme.barsBg, + ), + secondChild: const SizedBox( + height: 16, + width: 16, + ), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + context.translations.alsoSendAsDirectMessageLabel, + style: _streamChatTheme.textTheme.footnote.copyWith( + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), + ), + ), + ), + ], + ); + + Widget _animateSendButton(BuildContext context) { + late Widget sendButton; + if (_timeOut > 0) { + sendButton = _CountdownButton(count: _timeOut); + } else if (!_messageIsPresent && _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 _buildExpandActionsButton(BuildContext context) { + final channel = StreamChannel.of(context).channel; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: AnimatedCrossFade( + crossFadeState: _actionsShrunk + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstCurve: Curves.easeOut, + secondCurve: Curves.easeIn, + firstChild: IconButton( + onPressed: () { + if (_actionsShrunk) { + setState(() => _actionsShrunk = false); + } + }, + icon: Transform.rotate( + angle: (widget.actionsLocation == ActionsLocation.right || + widget.actionsLocation == ActionsLocation.rightInside) + ? pi + : 0, + child: StreamSvgIcon.emptyCircleLeft( + color: _messageInputTheme.expandButtonColor, + ), + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + ), + secondChild: widget.disableAttachments && + !widget.showCommandsButton && + !widget.actions.isNotEmpty + ? const Offstage() + : Wrap( + children: [ + if (!widget.disableAttachments) + _buildAttachmentButton(context), + if (widget.showCommandsButton && + widget.editMessage == null && + channel.state != null && + channel.config?.commands.isNotEmpty == true) + _buildCommandButton(context), + ...widget.actions, + ].insertBetween(const SizedBox(width: 8)), + ), + duration: const Duration(milliseconds: 300), + alignment: Alignment.center, + ), + ); + } + + Expanded _buildTextInput(BuildContext context) { + final margin = (widget.sendButtonLocation == SendButtonLocation.inside + ? const EdgeInsets.only(right: 8) + : EdgeInsets.zero) + + (widget.actionsLocation != ActionsLocation.left || _commandEnabled + ? const EdgeInsets.only(left: 8) + : EdgeInsets.zero); + return Expanded( + child: Container( + clipBehavior: Clip.hardEdge, + margin: margin, + decoration: BoxDecoration( + borderRadius: _messageInputTheme.borderRadius, + gradient: _focusNode.hasFocus + ? _messageInputTheme.activeBorderGradient + : _messageInputTheme.idleBorderGradient, + ), + child: Padding( + padding: const EdgeInsets.all(1.5), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: _messageInputTheme.borderRadius, + color: _messageInputTheme.inputBackgroundColor, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildReplyToMessage(), + _buildAttachments(), + LimitedBox( + maxHeight: widget.maxHeight, + child: TextField( + key: const Key('messageInputText'), + enabled: _inputEnabled, + maxLines: null, + onSubmitted: (_) => sendMessage(), + keyboardType: widget.keyboardType, + controller: textEditingController, + focusNode: _focusNode, + style: _messageInputTheme.inputTextStyle, + autofocus: widget.autofocus, + textAlignVertical: TextAlignVertical.center, + decoration: _getInputDecoration(context), + textCapitalization: TextCapitalization.sentences, + ), + ), + ], + ), + ), + ), + ), + ); + } + + InputDecoration _getInputDecoration(BuildContext context) { + final passedDecoration = _messageInputTheme.inputDecoration; + return InputDecoration( + isDense: true, + hintText: _getHint(context), + hintStyle: _messageInputTheme.inputTextStyle!.copyWith( + color: _streamChatTheme.colorTheme.textLowEmphasis, + ), + border: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + errorBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + disabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11), + prefixIcon: _commandEnabled + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Container( + constraints: BoxConstraints.tight(const Size(64, 24)), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: _streamChatTheme.colorTheme.accentPrimary, + ), + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamSvgIcon.lightning( + color: Colors.white, + size: 16, + ), + Text( + _chosenCommand?.name.toUpperCase() ?? '', + style: + _streamChatTheme.textTheme.footnoteBold.copyWith( + color: Colors.white, + ), + ), + ], + ), + ), + ), + ], + ) + : (widget.actionsLocation == ActionsLocation.leftInside + ? Row( + mainAxisSize: MainAxisSize.min, + children: [_buildExpandActionsButton(context)], + ) + : null), + suffixIconConstraints: const BoxConstraints.tightFor(height: 40), + prefixIconConstraints: const BoxConstraints.tightFor(height: 40), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_commandEnabled) + Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + icon: StreamSvgIcon.closeSmall(), + splashRadius: 24, + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + onPressed: () { + setState(() => _commandEnabled = false); + }, + ), + ), + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.rightInside) + _buildExpandActionsButton(context), + if (widget.sendButtonLocation == SendButtonLocation.inside) + _animateSendButton(context), + ], + ), + ).merge(passedDecoration); + } + + late final _onChangedDebounced = debounce( + () { + var value = textEditingController.text; + if (!mounted) return; + value = value.trim(); + + final channel = StreamChannel.of(context).channel; + if (value.isNotEmpty) { + // ignore: no-empty-block + channel.keyStroke(widget.parentMessage?.id).catchError((e) {}); + } + + var actionsLength = widget.actions.length; + if (widget.showCommandsButton) actionsLength += 1; + if (!widget.disableAttachments) actionsLength += 1; + + setState(() { + _actionsShrunk = value.isNotEmpty && actionsLength > 1; + }); + + _checkCommands(value, context); + _checkMentions(value, context); + _checkEmoji(value, context); + }, + const Duration(milliseconds: 350), + leading: true, + ); + + String _getHint(BuildContext context) { + if (_commandEnabled && _chosenCommand!.name == 'giphy') { + return context.translations.searchGifLabel; + } + if (_attachments.isNotEmpty) { + return context.translations.addACommentOrSendLabel; + } + if (_timeOut != 0) { + return context.translations.slowModeOnLabel; + } + + return context.translations.writeAMessageLabel; + } + + void _checkEmoji(String s, BuildContext context) { + if (s.isNotEmpty && + textEditingController.selection.baseOffset > 0 && + textEditingController.text + .substring(0, textEditingController.selection.baseOffset) + .contains(':')) { + final textToSelection = textEditingController.text + .substring(0, textEditingController.value.selection.start); + final splits = textToSelection.split(':'); + final query = splits[splits.length - 2].toLowerCase(); + final emoji = Emoji.byName(query); + + if (textToSelection.endsWith(':') && emoji != null) { + _chooseEmoji(splits.sublist(0, splits.length - 1), emoji); + } + } + } + + void _checkMentions(String s, BuildContext context) { + if (s.isNotEmpty && + textEditingController.selection.baseOffset > 0 && + textEditingController.text + .substring(0, textEditingController.selection.baseOffset) + .split(' ') + .last + .contains('@')) { + if (!_showMentionsOverlay) { + setState(() { + _showMentionsOverlay = true; + }); + } + } else if (_showMentionsOverlay) { + setState(() { + _showMentionsOverlay = false; + }); + } + } + + void _checkCommands(String s, BuildContext context) { + if (s.startsWith('/')) { + final allCommands = StreamChannel.of(context).channel.config?.commands; + final command = + allCommands?.firstWhereOrNull((it) => it.name == s.substring(1)); + if (command != null) { + return _setCommand(command); + } else if (!_showCommandsOverlay) { + setState(() { + _showCommandsOverlay = true; + }); + } + } else if (_showCommandsOverlay) { + setState(() { + _showCommandsOverlay = false; + }); + } + } + + Widget _buildCommandsOverlayEntry() { + final text = textEditingController.text.trimLeft(); + + final renderObject = context.findRenderObject() as RenderBox?; + if (renderObject == null) { + return const Offstage(); + } + return StreamCommandsOverlay( + channel: StreamChannel.of(context).channel, + size: Size(renderObject.size.width - 16, 400), + text: text, + onCommandResult: _setCommand, + ); + } + + Widget _buildFilePickerSection() { + final _attachmentContainsFile = + _attachments.values.any((it) => it.type == 'file'); + + final attachmentLimitCrossed = + _attachments.length >= widget.attachmentLimit; + + Color _getIconColor(int index) { + final streamChatThemeData = _streamChatTheme; + switch (index) { + case 0: + return _attachments.isEmpty + ? streamChatThemeData.colorTheme.accentPrimary + : (!_attachmentContainsFile + ? streamChatThemeData.colorTheme.accentPrimary + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2)); + case 1: + return _attachmentContainsFile + ? streamChatThemeData.colorTheme.accentPrimary + : (_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 && _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 && _attachments.isNotEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); + default: + return Colors.black; + } + } + + 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 && _attachments.isNotEmpty + ? null + : () { + setState(() { + _filePickerIndex = 0; + }); + }, + ), + IconButton( + iconSize: 32, + icon: StreamSvgIcon.files( + color: _getIconColor(1), + ), + onPressed: + !_attachmentContainsFile && _attachments.isNotEmpty + ? null + : () { + pickFile(DefaultAttachmentTypes.file); + }, + ), + IconButton( + icon: StreamSvgIcon.camera( + color: _getIconColor(2), + ), + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && + _attachments.isNotEmpty) + ? null + : () { + pickFile( + DefaultAttachmentTypes.image, + camera: true, + ); + }, + ), + IconButton( + padding: const EdgeInsets.all(0), + icon: StreamSvgIcon.record( + color: _getIconColor(3), + ), + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && + _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: _attachments.keys.toList(), + onAddMoreFilesClick: pickFile, + onMediaSelected: (media) { + if (_attachments.containsKey(media.id)) { + setState(() => _attachments.remove(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 StreamVideoService.compressVideo( + file.path!, + frameRate: widget.compressedVideoFrameRate, + quality: widget.compressedVideoQuality, + ); + + if (mediaInfo == null || + 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() { + final channel = StreamChannel.of(context).channel; + if (textEditingController.value.selection.start < 0 || + channel.state == null) { + return const Offstage(); + } + + final splits = textEditingController.text + .substring(0, textEditingController.value.selection.start) + .split('@'); + final query = splits.last.toLowerCase(); + + // ignore: cast_nullable_to_non_nullable + final renderObject = context.findRenderObject() as RenderBox; + + var tileBuilder = widget.userMentionsTileBuilder; + if (tileBuilder == null && widget.mentionsTileBuilder != null) { + tileBuilder = (context, user) { + final member = Member( + user: user, + userId: user.id, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + ); + return widget.mentionsTileBuilder!(context, member); + }; + } + + return LayoutBuilder( + builder: (context, snapshot) => StreamUserMentionsOverlay( + query: query, + mentionAllAppUsers: widget.mentionAllAppUsers, + client: StreamChat.of(context).client, + channel: channel, + size: Size( + renderObject.size.width - 16, + min(400, (snapshot.maxHeight - renderObject.size.height - 16).abs()), + ), + mentionsTileBuilder: tileBuilder, + onMentionUserTap: (user) { + _mentionedUsers.add(user); + splits[splits.length - 1] = user.name; + final rejoin = splits.join('@'); + + textEditingController.value = TextEditingValue( + text: rejoin + + textEditingController.text.substring( + textEditingController.selection.start, + ), + selection: TextSelection.collapsed( + offset: rejoin.length, + ), + ); + _onChangedDebounced.cancel(); + + setState(() => _showMentionsOverlay = false); + }, + ), + ); + } + + Widget _buildEmojiOverlay() { + if (textEditingController.value.selection.baseOffset < 0) { + return const Offstage(); + } + + final splits = textEditingController.text + .substring(0, textEditingController.value.selection.baseOffset) + .split(':'); + + final query = splits.last.toLowerCase(); + // ignore: cast_nullable_to_non_nullable + final renderObject = context.findRenderObject() as RenderBox; + + return StreamEmojiOverlay( + size: Size(renderObject.size.width - 16, 200), + query: query, + onEmojiResult: (emoji) { + _chooseEmoji(splits, emoji); + }, + ); + } + + void _chooseEmoji(List splits, Emoji emoji) { + final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; + + textEditingController.value = TextEditingValue( + text: rejoin + + textEditingController.text + .substring(textEditingController.selection.start), + selection: TextSelection.collapsed( + offset: rejoin.length, + ), + ); + } + + void _setCommand(Command c) { + textEditingController.clear(); + setState(() { + _chosenCommand = c; + _commandEnabled = true; + _showCommandsOverlay = false; + }); + } + + Widget _buildReplyToMessage() { + if (!_hasQuotedMessage) return const Offstage(); + final containsUrl = widget.quotedMessage!.attachments + .any((element) => element.titleLink != null); + return StreamQuotedMessageWidget( + reverse: true, + showBorder: !containsUrl, + message: widget.quotedMessage!, + messageTheme: _streamChatTheme.otherMessageTheme, + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + ); + } + + Widget _buildAttachments() { + if (_attachments.isEmpty) return const Offstage(); + final fileAttachments = _attachments.values + .where((it) => it.type == 'file') + .toList(growable: false); + final remainingAttachments = _attachments.values + .where((it) => it.type != 'file') + .toList(growable: false); + return Column( + children: [ + if (fileAttachments.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: LimitedBox( + maxHeight: 136, + child: ListView( + reverse: true, + shrinkWrap: true, + children: fileAttachments.reversed + .map( + (e) => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: StreamFileAttachment( + message: Message(), // dummy message + attachment: e, + size: Size( + MediaQuery.of(context).size.width * 0.65, + 56, + ), + trailing: Padding( + padding: const EdgeInsets.all(8), + child: _buildRemoveButton(e), + ), + ), + ), + ) + .insertBetween(const SizedBox(height: 8)), + ), + ), + ), + if (remainingAttachments.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: LimitedBox( + maxHeight: 104, + child: ListView( + scrollDirection: Axis.horizontal, + children: remainingAttachments + .map( + (attachment) => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Stack( + children: [ + AspectRatio( + aspectRatio: 1, + child: SizedBox( + height: 104, + width: 104, + child: _buildAttachment(attachment), + ), + ), + Positioned( + top: 8, + right: 8, + child: _buildRemoveButton(attachment), + ), + ], + ), + ), + ) + .insertBetween(const SizedBox(width: 8)), + ), + ), + ), + ], + ); + } + + Widget _buildRemoveButton(Attachment attachment) => SizedBox( + height: 24, + width: 24, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: () { + setState(() => _attachments.remove(attachment.id)); + }, + fillColor: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), + child: Center( + child: StreamSvgIcon.close( + size: 24, + color: _streamChatTheme.colorTheme.barsBg, + ), + ), + ), + ); + + Widget _buildAttachment(Attachment attachment) { + if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) == + true) { + return widget.attachmentThumbnailBuilders![attachment.type!]!( + context, + attachment, + ); + } + + switch (attachment.type) { + case 'image': + case 'giphy': + return attachment.file != null + ? Image.memory( + attachment.file!.bytes!, + fit: BoxFit.cover, + errorBuilder: (context, _, __) => Image.asset( + 'images/placeholder.png', + package: 'stream_chat_flutter', + ), + ) + : CachedNetworkImage( + imageUrl: attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl!, + fit: BoxFit.cover, + errorWidget: (_, obj, trace) => + getFileTypeImage(attachment.extraData['other'] as String?), + placeholder: (context, _) => Shimmer.fromColors( + baseColor: _streamChatTheme.colorTheme.disabled, + highlightColor: _streamChatTheme.colorTheme.inputBg, + child: Image.asset( + 'images/placeholder.png', + fit: BoxFit.cover, + package: 'stream_chat_flutter', + ), + ), + ); + case 'video': + return Stack( + children: [ + StreamVideoThumbnailImage( + height: 104, + width: 104, + video: (attachment.file?.path ?? attachment.assetUrl)!, + fit: BoxFit.cover, + ), + Positioned( + left: 8, + bottom: 10, + child: SvgPicture.asset( + 'svgs/video_call_icon.svg', + package: 'stream_chat_flutter', + ), + ), + ], + ); + default: + return Container( + color: Colors.black26, + child: const Icon(Icons.insert_drive_file), + ); + } + } + + Widget _buildCommandButton(BuildContext context) { + final s = textEditingController.text.trim(); + final defaultButton = IconButton( + icon: StreamSvgIcon.lightning( + color: s.isNotEmpty + ? _streamChatTheme.colorTheme.disabled + : (_showCommandsOverlay + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor), + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + if (_openFilePickerSection) { + setState(() => _openFilePickerSection = false); + await Future.delayed(const Duration(milliseconds: 300)); + } + + setState(() { + _showCommandsOverlay = !_showCommandsOverlay; + }); + }, + ); + + return widget.commandButtonBuilder?.call(context, defaultButton) ?? + defaultButton; + } + + Widget _buildAttachmentButton(BuildContext context) { + final defaultButton = IconButton( + icon: StreamSvgIcon.attach( + color: _openFilePickerSection + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor, + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + _showCommandsOverlay = false; + _showMentionsOverlay = false; + + if (_openFilePickerSection) { + setState(() => _openFilePickerSection = false); + } else { + showAttachmentModal(); + } + }, + ); + + return widget.attachmentButtonBuilder?.call(context, defaultButton) ?? + defaultButton; + } + + /// Show the attachment modal, making the user choose where to + /// pick a media from + void showAttachmentModal() { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + + if (!kIsWeb) { + setState(() { + _openFilePickerSection = true; + }); + } else { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + isScrollControlled: true, + builder: (_) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + context.translations.addAFileLabel, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + leading: const Icon(Icons.image), + title: Text(context.translations.uploadAPhotoLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.image); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.video_library), + title: Text(context.translations.uploadAVideoLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.video); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.insert_drive_file), + title: Text(context.translations.uploadAFileLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.file); + Navigator.pop(context); + }, + ), + ], + ), + ); + } + } + + /// 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) { + setState(() => _addAttachments([attachment])); + } + + /// Adds an attachment to the [_attachments] map + void _addAttachments(Iterable attachments) { + final limit = widget.attachmentLimit; + final length = _attachments.length + attachments.length; + if (length > limit) { + final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; + if (onAttachmentLimitExceed != null) { + return onAttachmentLimitExceed( + widget.attachmentLimit, + context.translations.attachmentLimitExceedError(limit), + ); + } + return _showErrorAlert( + context.translations.attachmentLimitExceedError(limit), + ); + } + for (final attachment in attachments) { + _attachments[attachment.id] = attachment; + } + } + + /// Pick a file from the device + /// If [camera] is true then the camera will open + void pickFile( + DefaultAttachmentTypes fileType, { + bool camera = false, + }) async { + setState(() => _inputEnabled = false); + + AttachmentFile? file; + String? attachmentType; + + if (fileType == DefaultAttachmentTypes.image) { + attachmentType = 'image'; + } else if (fileType == DefaultAttachmentTypes.video) { + attachmentType = 'video'; + } else if (fileType == DefaultAttachmentTypes.file) { + attachmentType = 'file'; + } + + if (camera) { + XFile? pickedFile; + if (fileType == DefaultAttachmentTypes.image) { + pickedFile = await _imagePicker.pickImage(source: ImageSource.camera); + } else if (fileType == DefaultAttachmentTypes.video) { + pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera); + } + if (pickedFile != null) { + final bytes = await pickedFile.readAsBytes(); + file = AttachmentFile( + size: bytes.length, + path: pickedFile.path, + bytes: bytes, + ); + } + } else { + late FileType type; + if (fileType == DefaultAttachmentTypes.image) { + type = FileType.image; + } else if (fileType == DefaultAttachmentTypes.video) { + type = FileType.video; + } else if (fileType == DefaultAttachmentTypes.file) { + type = FileType.any; + } + final res = await FilePicker.platform.pickFiles( + type: type, + ); + if (res?.files.isNotEmpty == true) { + file = res!.files.single.toAttachmentFile; + } + } + + setState(() => _inputEnabled = true); + + if (file == null) return; + + final mimeType = file.name?.mimeType ?? file.path!.split('/').last.mimeType; + + final extraDataMap = {}; + + if (mimeType?.subtype != null) { + extraDataMap['mime_type'] = mimeType!.subtype.toLowerCase(); + } + + extraDataMap['file_size'] = file.size!; + + final attachment = Attachment( + file: file, + type: attachmentType, + uploadState: const UploadState.preparing(), + extraData: extraDataMap, + ); + + if (file.size! > widget.maxAttachmentSize) { + if (attachmentType == 'video' && file.path != null) { + final mediaInfo = await (StreamVideoService.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(() { + _addAttachments([ + attachment.copyWith( + file: file, + extraData: {...attachment.extraData} + ..update('file_size', ((_) => file!.size!)), + ), + ]); + }); + } + + 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 = textEditingController.text.trim(); + if (text.isEmpty && _attachments.isEmpty) { + return; + } + + var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; + + shouldKeepFocus ??= !_commandEnabled; + + if (_commandEnabled) { + text = '${'/${_chosenCommand!.name} '}$text'; + } + + final attachments = [..._attachments.values]; + + textEditingController.clear(); + _attachments.clear(); + widget.onQuotedMessageCleared?.call(); + + setState(() { + _commandEnabled = false; + }); + + Message message; + if (widget.editMessage != null) { + message = widget.editMessage!.copyWith( + text: text, + attachments: attachments, + mentionedUsers: + _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(), + showInChannel: widget.parentMessage != null ? _sendAsDm : null, + ); + } + + if (widget.quotedMessage != null) { + message = message.copyWith( + quotedMessageId: widget.quotedMessage!.id, + ); + } + + if (widget.preMessageSending != null) { + message = await widget.preMessageSending!(message); + } + + final streamChannel = StreamChannel.of(context); + final channel = streamChannel.channel; + if (!channel.state!.isUpToDate) { + await streamChannel.reloadChannel(); + } + + _mentionedUsers.clear(); + + message = _replaceUserNameWithId(message); + + try { + Future sendingFuture; + if (widget.editMessage == null || + widget.editMessage!.status == MessageSendingStatus.failed || + widget.editMessage!.status == MessageSendingStatus.sending) { + sendingFuture = channel.sendMessage(message); + } else { + sendingFuture = channel.updateMessage(message); + } + + if (shouldKeepFocus) { + FocusScope.of(context).requestFocus(_focusNode); + } else { + FocusScope.of(context).unfocus(); + } + + final resp = await sendingFuture; + if (resp.message?.type == 'error') { + _parseExistingMessage(message); + } + _startSlowMode(); + widget.onMessageSent?.call(resp.message); + } catch (e, stk) { + if (widget.onError != null) { + widget.onError?.call(e, stk); + } else { + rethrow; + } + } + } + + void _showErrorAlert(String description) { + showModalBottomSheet( + backgroundColor: _streamChatTheme.colorTheme.barsBg, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + builder: (context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 26, + ), + StreamSvgIcon.error( + color: _streamChatTheme.colorTheme.accentError, + size: 24, + ), + const SizedBox( + height: 26, + ), + Text( + context.translations.somethingWentWrongError, + style: _streamChatTheme.textTheme.headlineBold, + ), + const SizedBox( + height: 7, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + description, + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 36, + ), + Container( + color: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.08), + height: 1, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + context.translations.okLabel, + style: _streamChatTheme.textTheme.bodyBold.copyWith( + color: _streamChatTheme.colorTheme.accentPrimary, + ), + ), + ), + ], + ), + ], + ), + ); + } + + void _parseExistingMessage(Message message) { + final messageText = message.text; + if (messageText != null) textEditingController.text = messageText; + _addAttachments(message.attachments); + } + + @override + void dispose() { + textEditingController.dispose(); + _focusNode.removeListener(_focusNodeListener); + if (_isInternalFocusNode) _focusNode.dispose(); + _stopSlowMode(); + _onChangedDebounced.cancel(); + super.dispose(); + } + + bool _initialized = false; + + @override + void didChangeDependencies() { + _streamChatTheme = StreamChatTheme.of(context); + _messageInputTheme = StreamMessageInputTheme.of(context); + if (widget.editMessage == null) _startSlowMode(); + + if ((widget.editMessage != null || widget.initialMessage != null) && + !_initialized) { + FocusScope.of(context).requestFocus(_focusNode); + _initialized = true; + } + 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 StreamMediaListView( + 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, + ), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +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'), + ), + ), + ), + ); +} + +Message _replaceUserNameWithId(Message message) { + final mentionedUsers = message.mentionedUsers; + if (mentionedUsers.isEmpty) return message; + + var messageTextToSend = message.text; + if (messageTextToSend == null) return message; + + for (final user in mentionedUsers.toSet()) { + final userName = user.name; + messageTextToSend = messageTextToSend!.replaceAll( + '@$userName', + '@${user.id}', + ); + } + + return message.copyWith(text: messageTextToSend); +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart b/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart index 50f18e50..aaf016a3 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart @@ -2,9 +2,9 @@ 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({ +class StreamCountdownButton extends StatelessWidget { + /// Constructor for creating [StreamCountdownButton]. + const StreamCountdownButton({ Key? key, required this.count, }) : super(key: key); 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 a9f09bbe..e7ec69f1 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 @@ -28,7 +28,7 @@ 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]. +/// A callback that can be passed to [StreamMessageInput.onError]. /// /// This callback should not throw. /// @@ -38,7 +38,7 @@ typedef ErrorListener = void Function( StackTrace? stackTrace, ); -/// A callback that can be passed to [MessageInput.onAttachmentLimitExceed]. +/// A callback that can be passed to [StreamMessageInput.onAttachmentLimitExceed]. /// /// This callback should not throw. /// @@ -63,7 +63,7 @@ typedef MentionTileBuilder = Widget Function( /// Builder function for building a user mention tile. /// -/// Use [UserMentionTile] for the default implementation. +/// Use [StreamUserMentionTile] for the default implementation. typedef UserMentionTileBuilder = Widget Function( BuildContext context, User user, @@ -92,7 +92,7 @@ typedef AttachmentsPickerBuilder = Widget Function( StreamAttachmentPicker defaultPicker, ); -/// Location for actions on the [MessageInput]. +/// Location for actions on the [StreamMessageInput]. enum ActionsLocation { /// Align to left left, @@ -173,14 +173,14 @@ const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes /// } /// ``` /// -/// You usually put this widget in the same page of a [MessageListView] +/// You usually put this widget in the same page of a [StreamMessageListView] /// as the bottom widget. /// /// The widget renders the ui based on the first ancestor of /// type [StreamChatTheme]. Modify it to change the widget appearance. -class MessageInput extends StatefulWidget { +class StreamMessageInput extends StatefulWidget { /// Instantiate a new MessageInput - const MessageInput({ + const StreamMessageInput({ Key? key, this.onMessageSent, this.preMessageSending, @@ -325,7 +325,7 @@ class MessageInput extends StatefulWidget { /// Builder for creating send button final MessageRelatedBuilder? sendButtonBuilder; - /// Defines if the [MessageInput] loses focuses after a message is sent. + /// Defines if the [StreamMessageInput] loses focuses after a message is sent. /// The default behaviour keeps focus until a command is enabled. final bool? shouldKeepFocusAfterMessage; @@ -335,25 +335,25 @@ class MessageInput extends StatefulWidget { /// Restoration ID to save and restore the state of the MessageInput. final String? restorationId; - /// Wrap [MessageInput] with a [SafeArea widget] + /// Wrap [StreamMessageInput] with a [SafeArea widget] final bool? enableSafeArea; - /// Elevation of the [MessageInput] + /// Elevation of the [StreamMessageInput] final double? elevation; - /// Shadow for the [MessageInput] widget + /// Shadow for the [StreamMessageInput] widget final BoxShadow? shadow; static bool _defaultValidator(Message message) => message.text?.isNotEmpty == true || message.attachments.isNotEmpty; @override - MessageInputState createState() => MessageInputState(); + StreamMessageInputState createState() => StreamMessageInputState(); } -/// State of [MessageInput] -class MessageInputState extends State - with RestorationMixin { +/// State of [StreamMessageInput] +class StreamMessageInputState extends State + with RestorationMixin { final _imagePicker = ImagePicker(); late FocusNode _focusNode = widget.focusNode ?? FocusNode(); late final _isInternalFocusNode = widget.focusNode == null; @@ -367,7 +367,7 @@ class MessageInputState extends State bool _openFilePickerSection = false; late StreamChatThemeData _streamChatTheme; - late MessageInputThemeData _messageInputTheme; + late StreamMessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => _effectiveController.value.quotedMessage != null; @@ -410,7 +410,7 @@ class MessageInputState extends State } @override - void didUpdateWidget(covariant MessageInput oldWidget) { + void didUpdateWidget(covariant StreamMessageInput oldWidget) { super.didUpdateWidget(oldWidget); if (widget.messageInputController == null && oldWidget.messageInputController != null) { @@ -596,7 +596,7 @@ class MessageInputState extends State child: child, ); } - return MultiOverlay( + return StreamMultiOverlay( childAnchor: Alignment.topCenter, overlayAnchor: Alignment.bottomCenter, overlayOptions: [ @@ -1109,7 +1109,7 @@ class MessageInputState extends State if (renderObject == null) { return const Offstage(); } - return CommandsOverlay( + return StreamCommandsOverlay( channel: StreamChannel.of(context).channel, size: Size(renderObject.size.width - 16, 400), text: text, @@ -1170,7 +1170,7 @@ class MessageInputState extends State } return LayoutBuilder( - builder: (context, snapshot) => UserMentionsOverlay( + builder: (context, snapshot) => StreamUserMentionsOverlay( query: query, mentionAllAppUsers: widget.mentionAllAppUsers, client: StreamChat.of(context).client, @@ -1210,7 +1210,7 @@ class MessageInputState extends State // ignore: cast_nullable_to_non_nullable final renderObject = context.findRenderObject() as RenderBox; - return EmojiOverlay( + return StreamEmojiOverlay( size: Size(renderObject.size.width - 16, 200), query: query, onEmojiResult: (emoji) { @@ -1241,7 +1241,7 @@ class MessageInputState extends State if (!_hasQuotedMessage) return const Offstage(); final containsUrl = _effectiveController.value.quotedMessage!.attachments .any((element) => element.titleLink != null); - return QuotedMessageWidget( + return StreamQuotedMessageWidget( reverse: true, showBorder: !containsUrl, message: _effectiveController.value.quotedMessage!, @@ -1275,7 +1275,7 @@ class MessageInputState extends State .map( (e) => ClipRRect( borderRadius: BorderRadius.circular(10), - child: FileAttachment( + child: StreamFileAttachment( message: Message(), // dummy message attachment: e, size: Size( @@ -1397,7 +1397,7 @@ class MessageInputState extends State case 'video': return Stack( children: [ - VideoThumbnailImage( + StreamVideoThumbnailImage( height: 104, width: 104, video: (attachment.file?.path ?? attachment.assetUrl)!, @@ -1640,7 +1640,7 @@ class MessageInputState extends State if (file.size! > widget.maxAttachmentSize) { if (attachmentType == 'video' && file.path != null) { - final mediaInfo = await (VideoService.compressVideo( + final mediaInfo = await (StreamVideoService.compressVideo( file.path!, frameRate: widget.compressedVideoFrameRate, quality: widget.compressedVideoQuality, @@ -1831,7 +1831,7 @@ class MessageInputState extends State @override void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); - _messageInputTheme = MessageInputTheme.of(context); + _messageInputTheme = StreamMessageInputTheme.of(context); super.didChangeDependencies(); } 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 909f9561..1e1b99c7 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 @@ -376,7 +376,7 @@ class _StreamAttachmentPickerState extends State { if (file.size! > widget.maxAttachmentSize) { if (medium.type == AssetType.video && file.path != null) { - final mediaInfo = await (VideoService.compressVideo( + final mediaInfo = await (StreamVideoService.compressVideo( file.path!, frameRate: widget.compressedVideoFrameRate, quality: widget.compressedVideoQuality, @@ -507,7 +507,7 @@ class _PickerWidgetState extends State<_PickerWidget> { ), ); } - return MediaListView( + return StreamMediaListView( selectedIds: widget.selectedMedias, onSelect: widget.onMediaSelected, ); 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 9916f6f7..67a06f67 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 @@ -44,7 +44,7 @@ class StreamMessageSendButton extends StatelessWidget { late Widget sendButton; if (timeOut > 0) { - sendButton = CountdownButton(count: timeOut); + sendButton = StreamCountdownButton(count: timeOut); } else if (isIdle) { sendButton = idleSendButton ?? _buildIdleSendButton(context); } else { @@ -63,7 +63,7 @@ class StreamMessageSendButton extends StatelessWidget { } Widget _buildIdleSendButton(BuildContext context) { - final _messageInputTheme = MessageInputTheme.of(context); + final _messageInputTheme = StreamMessageInputTheme.of(context); return Padding( padding: const EdgeInsets.all(8), @@ -75,7 +75,7 @@ class StreamMessageSendButton extends StatelessWidget { } Widget _buildSendButton(BuildContext context) { - final _messageInputTheme = MessageInputTheme.of(context); + final _messageInputTheme = StreamMessageInputTheme.of(context); return Padding( padding: const EdgeInsets.all(8), diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index e4d67740..d38d7288 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -11,22 +11,22 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:visibility_detector/visibility_detector.dart'; /// Widget builder for message -/// [defaultMessageWidget] is the default [MessageWidget] configuration +/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration /// Use [defaultMessageWidget.copyWith] to easily customize it typedef MessageBuilder = Widget Function( BuildContext, MessageDetails, List, - MessageWidget defaultMessageWidget, + StreamMessageWidget defaultMessageWidget, ); /// Widget builder for parent message -/// [defaultMessageWidget] is the default [MessageWidget] configuration +/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration /// Use [defaultMessageWidget.copyWith] to easily customize it typedef ParentMessageBuilder = Widget Function( BuildContext, Message?, - MessageWidget defaultMessageWidget, + StreamMessageWidget defaultMessageWidget, ); /// Widget builder for system message @@ -123,6 +123,11 @@ class MessageDetails { final int index; } +/// {@macro message_list_view} +@Deprecated("Use 'StreamMessageListView' instead") +typedef MessageListView = StreamMessageListView; + +/// {@template message_list_view} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview_paint.png) /// @@ -165,9 +170,10 @@ class MessageDetails { /// The widget components render the ui based on the first /// ancestor of type [StreamChatTheme]. /// Modify it to change the widget appearance. -class MessageListView extends StatefulWidget { +/// {@endtemplate} +class StreamMessageListView extends StatefulWidget { /// Instantiate a new MessageListView - const MessageListView({ + const StreamMessageListView({ Key? key, this.showScrollToBottom = true, this.messageBuilder, @@ -330,10 +336,10 @@ class MessageListView extends StatefulWidget { final SpacingWidgetBuilder? spacingWidgetBuilder; @override - _MessageListViewState createState() => _MessageListViewState(); + _StreamMessageListViewState createState() => _StreamMessageListViewState(); } -class _MessageListViewState extends State { +class _StreamMessageListViewState extends State { ItemScrollController? _scrollController; void Function(Message)? _onThreadTap; final ValueNotifier _showScrollToBottom = ValueNotifier(false); @@ -461,7 +467,7 @@ class _MessageListViewState extends State { final child = Stack( alignment: Alignment.center, children: [ - ConnectionStatusBuilder( + StreamConnectionStatusBuilder( statusBuilder: (context, status) { var statusString = ''; var showStatus = true; @@ -478,7 +484,7 @@ class _MessageListViewState extends State { break; } - return InfoTile( + return StreamInfoTile( showMessage: widget.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, @@ -592,7 +598,7 @@ class _MessageListViewState extends State { ) : Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: DateDivider( + child: StreamDateDivider( dateTime: nextMessage.createdAt.toLocal(), ), ); @@ -730,8 +736,10 @@ class _MessageListViewState extends State { ], ); - final backgroundColor = MessageListViewTheme.of(context).backgroundColor; - final backgroundImage = MessageListViewTheme.of(context).backgroundImage; + final backgroundColor = + StreamMessageListViewTheme.of(context).backgroundColor; + final backgroundImage = + StreamMessageListViewTheme.of(context).backgroundImage; if (backgroundColor != null || backgroundImage != null) { return Container( @@ -761,7 +769,7 @@ class _MessageListViewState extends State { child: Text( context.translations.threadSeparatorText(replyCount), textAlign: TextAlign.center, - style: ChannelHeaderTheme.of(context).subtitleStyle, + style: StreamChannelHeaderTheme.of(context).subtitleStyle, ), ), ); @@ -815,7 +823,7 @@ class _MessageListViewState extends State { final message = messages[index - 2]; return widget.dateDividerBuilder != null ? widget.dateDividerBuilder!(message.createdAt.toLocal()) - : DateDivider(dateTime: message.createdAt.toLocal()); + : StreamDateDivider(dateTime: message.createdAt.toLocal()); }, ), ); @@ -976,7 +984,7 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); - final defaultMessageWidget = MessageWidget( + final defaultMessageWidget = StreamMessageWidget( showReplyMessage: false, showResendMessage: false, showThreadReplyMessage: false, @@ -1040,7 +1048,7 @@ class _MessageListViewState extends State { if ((message.type == 'system' || message.type == 'error') && message.text?.isNotEmpty == true) { return widget.systemMessageBuilder?.call(context, message) ?? - SystemMessage( + StreamSystemMessage( message: message, onMessageTap: (message) { if (widget.onSystemMessageTap != null) { @@ -1110,7 +1118,7 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); - Widget messageWidget = MessageWidget( + Widget messageWidget = StreamMessageWidget( message: message, reverse: isMyMessage, showReactions: !message.isDeleted, @@ -1226,7 +1234,7 @@ class _MessageListViewState extends State { index, ), messages, - messageWidget as MessageWidget, + messageWidget as StreamMessageWidget, ); } diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 7568c565..eceba3f2 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -5,10 +5,16 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro message_reactions_modal} +@Deprecated("Use 'StreamMessageReactionsModal' instead") +typedef MessageReactionsModal = StreamMessageReactionsModal; + +/// {@template message_reactions_modal} /// Modal widget for displaying message reactions -class MessageReactionsModal extends StatelessWidget { - /// Constructor for creating a [MessageReactionsModal] reactions - const MessageReactionsModal({ +/// {@endtemplate} +class StreamMessageReactionsModal extends StatelessWidget { + /// Constructor for creating a [StreamMessageReactionsModal] reactions + const StreamMessageReactionsModal({ Key? key, required this.message, required this.messageWidget, @@ -24,8 +30,8 @@ class MessageReactionsModal extends StatelessWidget { /// Message to display reactions of final Message message; - /// [MessageThemeData] to apply to [message] - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] to apply to [message] + final StreamMessageThemeData messageTheme; /// Flag to reverse message final bool reverse; @@ -88,7 +94,7 @@ class MessageReactionsModal extends StatelessWidget { : -(1.2 - divFactor)), 0, ), - child: ReactionPicker( + child: StreamReactionPicker( message: message, ), ), @@ -200,7 +206,7 @@ class MessageReactionsModal extends StatelessWidget { Stack( clipBehavior: Clip.none, children: [ - UserAvatar( + StreamUserAvatar( onTap: onUserAvatarTap, user: reaction.user!, constraints: const BoxConstraints.tightFor( @@ -220,7 +226,7 @@ class MessageReactionsModal extends StatelessWidget { child: Align( alignment: reverse ? Alignment.centerRight : Alignment.centerLeft, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [reaction], flipTail: !reverse, borderColor: diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 022ec7ce..fe6d02b7 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -2,17 +2,23 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro message_search_item} +@Deprecated("Use 'StreamMessageSearchItem' instead") +typedef MessageSearchItem = StreamMessageSearchItem; + +/// {@template message_search_item} /// It shows the current [Message] preview. /// /// Usually you don't use this widget as it's the default item used by -/// [MessageSearchListView]. +/// [StreamMessageSearchListView]. /// /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. -class MessageSearchItem extends StatelessWidget { +/// {@endtemplate} +class StreamMessageSearchItem extends StatelessWidget { /// Instantiate a new MessageSearchItem - const MessageSearchItem({ + const StreamMessageSearchItem({ Key? key, required this.getMessageResponse, this.onTap, @@ -25,7 +31,7 @@ class MessageSearchItem extends StatelessWidget { /// Function called when tapping this widget final VoidCallback? onTap; - /// If true the [MessageSearchItem] will show the current online Status + /// If true the [StreamMessageSearchItem] will show the current online Status final bool showOnlineStatus; @override @@ -34,10 +40,10 @@ class MessageSearchItem extends StatelessWidget { final channel = getMessageResponse.channel; final channelName = channel?.extraData['name']; final user = message.user!; - final channelPreviewTheme = ChannelPreviewTheme.of(context); + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); return ListTile( onTap: onTap, - leading: UserAvatar( + leading: StreamUserAvatar( user: user, showOnlineStatus: showOnlineStatus, constraints: const BoxConstraints.tightFor( @@ -120,7 +126,7 @@ class MessageSearchItem extends StatelessWidget { text = parts.join(' '); } - final channelPreviewTheme = ChannelPreviewTheme.of(context); + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); return Text.rich( _getDisplayText( text!, diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 76ae0e98..380b6545 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -11,13 +11,17 @@ typedef MessageSearchItemBuilder = Widget Function( GetMessageResponse, ); -/// Builder used when [MessageSearchListView] is empty +/// Builder used when [StreamMessageSearchListView] is empty typedef EmptyMessageSearchBuilder = Widget Function( BuildContext context, String searchQuery, ); -/// +/// {@macro message_search_list_view} +@Deprecated("Use 'StreamMessageSearchListView' instead") +typedef MessageSearchListView = StreamMessageSearchListView; + +/// {@template message_search_list_view} /// It shows the list of searched messages. /// /// ```dart @@ -47,9 +51,10 @@ typedef EmptyMessageSearchBuilder = Widget Function( /// The widget components render the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. -class MessageSearchListView extends StatefulWidget { +/// {@endtemplate} +class StreamMessageSearchListView extends StatefulWidget { /// Instantiate a new MessageSearchListView - const MessageSearchListView({ + const StreamMessageSearchListView({ Key? key, required this.filters, this.messageQuery, @@ -96,7 +101,7 @@ class MessageSearchListView extends StatefulWidget { /// Builder used to create a custom item preview final MessageSearchItemBuilder? itemBuilder; - /// Function called when tapping on a [MessageSearchItem] + /// Function called when tapping on a [StreamMessageSearchItem] final MessageSearchItemTapCallback? onItemTap; /// Builder used to create a custom item separator @@ -130,10 +135,12 @@ class MessageSearchListView extends StatefulWidget { final MessageSearchListController? messageSearchListController; @override - _MessageSearchListViewState createState() => _MessageSearchListViewState(); + _StreamMessageSearchListViewState createState() => + _StreamMessageSearchListViewState(); } -class _MessageSearchListViewState extends State { +class _StreamMessageSearchListViewState + extends State { late final _defaultController = MessageSearchListController(); MessageSearchListController get _messageSearchListController => @@ -168,7 +175,7 @@ class _MessageSearchListViewState extends State { if (error is Error) { print(error.stackTrace); } - return InfoTile( + return StreamInfoTile( showMessage: widget.showErrorTile, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, @@ -195,7 +202,7 @@ class _MessageSearchListViewState extends State { ); final backgroundColor = - MessageSearchListViewTheme.of(context).backgroundColor; + StreamMessageSearchListViewTheme.of(context).backgroundColor; if (backgroundColor != null) { return ColoredBox( @@ -219,7 +226,7 @@ class _MessageSearchListViewState extends State { if (widget.itemBuilder != null) { return widget.itemBuilder!(context, getMessageResponse); } - return MessageSearchItem( + return StreamMessageSearchItem( getMessageResponse: getMessageResponse, onTap: () => widget.onItemTap!(getMessageResponse), ); diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 4787bb9e..012aa5c7 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -3,10 +3,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro message_text} +@Deprecated("Use 'StreamMessageText' instead") +typedef MessageText = StreamMessageText; + +/// {@template message_text} /// Text widget to display in message -class MessageText extends StatelessWidget { - /// Constructor for creating a [MessageText] widget - const MessageText({ +/// {@endtemplate} +class StreamMessageText extends StatelessWidget { + /// Constructor for creating a [StreamMessageText] widget + const StreamMessageText({ Key? key, required this.message, required this.messageTheme, @@ -23,8 +29,8 @@ class MessageText extends StatelessWidget { /// Callback for when link is tapped final void Function(String)? onLinkTap; - /// [MessageThemeData] whose text theme is to be applied - final MessageThemeData messageTheme; + /// [StreamMessageThemeData] whose text theme is to be applied + final StreamMessageThemeData messageTheme; @override Widget build(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 864c977e..22a7ef18 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -32,20 +32,26 @@ enum DisplayWidget { show, } +/// {@macro message_widget} +@Deprecated("Use 'StreamMessageWidget' instead") +typedef MessageWidget = StreamMessageWidget; + +/// {@template message_widget} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget_paint.png) /// /// It shows a message with reactions, replies and user avatar. /// /// Usually you don't use this widget as it's the default message widget used by -/// [MessageListView]. +/// [StreamMessageListView]. /// /// The widget components render the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. -class MessageWidget extends StatefulWidget { - /// - MessageWidget({ +/// {@endtemplate} +class StreamMessageWidget extends StatefulWidget { + /// Creates a new instance of the message widget. + StreamMessageWidget({ Key? key, required this.message, required this.messageTheme, @@ -121,7 +127,7 @@ class MessageWidget extends StatefulWidget { context, Material( color: messageTheme.messageBackgroundColor, - child: ImageGroup( + child: StreamImageGroup( size: Size( mediaQueryData.size.width * 0.8, mediaQueryData.size.height * 0.3, @@ -142,7 +148,7 @@ class MessageWidget extends StatefulWidget { return wrapAttachmentWidget( context, - ImageAttachment( + StreamImageAttachment( attachment: attachments[0], message: message, messageTheme: messageTheme, @@ -172,7 +178,7 @@ class MessageWidget extends StatefulWidget { Column( children: attachments.map((attachment) { final mediaQueryData = MediaQuery.of(context); - return VideoAttachment( + return StreamVideoAttachment( attachment: attachment, messageTheme: messageTheme, size: Size( @@ -204,7 +210,7 @@ class MessageWidget extends StatefulWidget { Column( children: attachments.map((attachment) { final mediaQueryData = MediaQuery.of(context); - return GiphyAttachment( + return StreamGiphyAttachment( attachment: attachment, message: message, size: Size( @@ -240,7 +246,7 @@ class MessageWidget extends StatefulWidget { final mediaQueryData = MediaQuery.of(context); return wrapAttachmentWidget( context, - FileAttachment( + StreamFileAttachment( message: message, attachment: attachment, size: Size( @@ -300,7 +306,7 @@ class MessageWidget extends StatefulWidget { final Message message; /// The message theme - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; /// If true the widget will be mirrored final bool reverse; @@ -356,7 +362,7 @@ class MessageWidget extends StatefulWidget { /// The function called when tapping on a link final void Function(String)? onLinkTap; - /// Used in [MessageReactionsModal] and [MessageActionsModal] + /// Used in [StreamMessageReactionsModal] and [StreamMessageActionsModal] final bool showReactionPickerIndicator; /// List of users who read @@ -417,13 +423,13 @@ class MessageWidget extends StatefulWidget { final void Function(Message)? onMessageTap; /// List of custom actions shown on message long tap - final List customActions; + final List customActions; /// Customize onTap on attachment final void Function(Message message, Attachment attachment)? onAttachmentTap; - /// Creates a copy of [MessageWidget] with specified attributes overridden. - MessageWidget copyWith({ + /// Creates a copy of [StreamMessageWidget] with specified attributes overridden. + StreamMessageWidget copyWith({ Key? key, void Function(User)? onMentionTap, void Function(Message)? onThreadTap, @@ -435,7 +441,7 @@ class MessageWidget extends StatefulWidget { Widget Function(BuildContext, Message)? deletedBottomRowBuilder, void Function(BuildContext, Message)? onMessageActions, Message? message, - MessageThemeData? messageTheme, + StreamMessageThemeData? messageTheme, bool? reverse, ShapeBorder? shape, ShapeBorder? attachmentShape, @@ -473,11 +479,11 @@ class MessageWidget extends StatefulWidget { bool? translateUserAvatar, OnQuotedMessageTap? onQuotedMessageTap, void Function(Message)? onMessageTap, - List? customActions, + List? customActions, void Function(Message message, Attachment attachment)? onAttachmentTap, Widget Function(BuildContext, User)? userAvatarBuilder, }) => - MessageWidget( + StreamMessageWidget( key: key ?? this.key, onMentionTap: onMentionTap ?? this.onMentionTap, onThreadTap: onThreadTap ?? this.onThreadTap, @@ -539,11 +545,11 @@ class MessageWidget extends StatefulWidget { ); @override - _MessageWidgetState createState() => _MessageWidgetState(); + _StreamMessageWidgetState createState() => _StreamMessageWidgetState(); } -class _MessageWidgetState extends State - with AutomaticKeepAliveClientMixin { +class _StreamMessageWidgetState extends State + with AutomaticKeepAliveClientMixin { bool get showThreadReplyIndicator => widget.showThreadReplyIndicator; bool get showSendingIndicator => widget.showSendingIndicator; @@ -717,7 +723,7 @@ class _MessageWidgetState extends State ? 0 : 4.0, ), - child: DeletedMessage( + child: StreamDeletedMessage( borderRadiusGeometry: widget .borderRadiusGeometry, borderSide: @@ -854,7 +860,7 @@ class _MessageWidgetState extends State ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) : null; final chatThemeData = _streamChatTheme; - return QuotedMessageWidget( + return StreamQuotedMessageWidget( onTap: onTap, message: widget.message.quotedMessage!, messageTheme: isMyMessage @@ -1008,7 +1014,7 @@ class _MessageWidgetState extends State getWebsiteName(hostName.toLowerCase()) ?? hostName.capitalize(); - return UrlAttachment( + return StreamUrlAttachment( urlAttachment: urlAttachment, hostDisplayName: hostDisplayName, textPadding: widget.textPadding, @@ -1041,7 +1047,7 @@ class _MessageWidgetState extends State child: _shouldShowReactions ? GestureDetector( onTap: () => _showMessageReactionsModalBottomSheet(context), - child: ReactionBubble( + child: StreamReactionBubble( key: ValueKey('${widget.message.id}.reactions'), reverse: widget.reverse, flipTail: widget.reverse, @@ -1072,7 +1078,7 @@ class _MessageWidgetState extends State barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: widget.copyWith( key: const Key('MessageWidget'), message: widget.message.copyWith( @@ -1129,7 +1135,7 @@ class _MessageWidgetState extends State barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, - child: MessageReactionsModal( + child: StreamMessageReactionsModal( messageWidget: widget.copyWith( key: const Key('MessageWidget'), message: widget.message.copyWith( @@ -1246,7 +1252,7 @@ class _MessageWidgetState extends State final channel = StreamChannel.of(context).channel; if (!channel.ownCapabilities.contains(PermissionType.readEvents)) { - return SendingIndicator( + return StreamSendingIndicator( message: message, size: style!.fontSize, ); @@ -1261,7 +1267,7 @@ class _MessageWidgetState extends State (it.lastRead.isAfter(message.createdAt) || it.lastRead.isAtSameMomentAs(message.createdAt))); final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1; - Widget child = SendingIndicator( + Widget child = StreamSendingIndicator( message: message, isMessageRead: isMessageRead, size: style!.fontSize, @@ -1295,7 +1301,7 @@ class _MessageWidgetState extends State : 0, ), child: widget.userAvatarBuilder?.call(context, widget.message.user!) ?? - UserAvatar( + StreamUserAvatar( user: widget.message.user!, onTap: widget.onUserAvatarTap, constraints: widget.messageTheme.avatarTheme!.constraints, @@ -1313,7 +1319,7 @@ class _MessageWidgetState extends State padding: isOnlyEmoji ? EdgeInsets.zero : widget.textPadding, child: widget.textBuilder != null ? widget.textBuilder!(context, widget.message) - : MessageText( + : StreamMessageText( onLinkTap: widget.onLinkTap, message: widget.message, onMentionTap: widget.onMentionTap, @@ -1430,7 +1436,7 @@ class _ThreadParticipants extends StatelessWidget { color: _streamChatTheme.colorTheme.barsBg, ), padding: const EdgeInsets.all(1), - child: UserAvatar( + child: StreamUserAvatar( user: user, constraints: BoxConstraints.loose(const Size.fromRadius(7)), showOnlineStatus: false, diff --git a/packages/stream_chat_flutter/lib/src/multi_overlay.dart b/packages/stream_chat_flutter/lib/src/multi_overlay.dart index 31b9d5e9..bd1ed387 100644 --- a/packages/stream_chat_flutter/lib/src/multi_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/multi_overlay.dart @@ -2,15 +2,21 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_portal/flutter_portal.dart'; +/// {@macro multi_overlay} +@Deprecated("Use 'StreamMultiOverlay' instead") +typedef MultiOverlay = StreamMultiOverlay; + +/// {@template multi_overlay} /// Widget that renders a single overlay widget from a list of [overlayOptions] /// It shows the first one that is visible -class MultiOverlay extends StatelessWidget { +/// {@endtemplate} +class StreamMultiOverlay extends StatelessWidget { /// Constructs a new MultiOverlay widget /// [overlayOptions] - the list of overlay options /// [overlayAnchor] - the anchor relative to the overlay /// [childAnchor] - the anchor relative to the child /// [child] - the child widget - const MultiOverlay({ + const StreamMultiOverlay({ Key? key, required this.overlayOptions, required this.child, diff --git a/packages/stream_chat_flutter/lib/src/option_list_tile.dart b/packages/stream_chat_flutter/lib/src/option_list_tile.dart index b8daa0e3..185d40f8 100644 --- a/packages/stream_chat_flutter/lib/src/option_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/option_list_tile.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro option_list_tile} +@Deprecated("Use 'StreamOptionListTile' instead") +typedef OptionListTile = StreamOptionListTile; + +/// {@template option_list_tile} /// List tile for [ChannelBottomSheet] -class OptionListTile extends StatelessWidget { - /// Constructor for creating [OptionListTile] - const OptionListTile({ +/// {@endtemplate} +class StreamOptionListTile extends StatelessWidget { + /// Constructor for creating [StreamOptionListTile] + const StreamOptionListTile({ Key? key, required this.title, this.leading, diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 131ca60b..81c8b892 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -10,54 +10,14 @@ typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( Attachment, ); -class _VideoAttachmentThumbnail extends StatefulWidget { - const _VideoAttachmentThumbnail({ - Key? key, - required this.attachment, - this.size = const Size(32, 32), - }) : super(key: key); +/// Widget for the quoted message. +@Deprecated("Use 'StreamQuotedMessageWidget' instead") +typedef QuotedMessageWidget = StreamQuotedMessageWidget; - final Size size; - final Attachment attachment; - - @override - _VideoAttachmentThumbnailState createState() => - _VideoAttachmentThumbnailState(); -} - -class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> { - late VideoPlayerController _controller; - - @override - void initState() { - super.initState(); - _controller = VideoPlayerController.network(widget.attachment.assetUrl!) - ..initialize().then((_) { - // ignore: no-empty-block - setState(() {}); //when your thumbnail will show. - }); - } - - @override - void dispose() { - super.dispose(); - _controller.dispose(); - } - - @override - Widget build(BuildContext context) => SizedBox( - height: widget.size.height, - width: widget.size.width, - child: _controller.value.isInitialized - ? VideoPlayer(_controller) - : const CircularProgressIndicator(), - ); -} - -/// -class QuotedMessageWidget extends StatelessWidget { - /// - const QuotedMessageWidget({ +/// Widget for the quoted message. +class StreamQuotedMessageWidget extends StatelessWidget { + /// Creates a new instance of the widget. + const StreamQuotedMessageWidget({ Key? key, required this.message, required this.messageTheme, @@ -73,7 +33,7 @@ class QuotedMessageWidget extends StatelessWidget { final Message message; /// The message theme - final MessageThemeData messageTheme; + final StreamMessageThemeData messageTheme; /// If true the widget will be mirrored final bool reverse; @@ -134,7 +94,7 @@ class QuotedMessageWidget extends StatelessWidget { if (_hasAttachments) _parseAttachments(context), if (msg.text!.isNotEmpty) Flexible( - child: MessageText( + child: StreamMessageText( message: msg, messageTheme: isOnlyEmoji && _containsText ? messageTheme.copyWith( @@ -231,7 +191,7 @@ class QuotedMessageWidget extends StatelessWidget { borderRadius: BorderRadius.circular(8), ); - Widget _buildUserAvatar() => UserAvatar( + Widget _buildUserAvatar() => StreamUserAvatar( user: message.user!, constraints: const BoxConstraints.tightFor( height: 24, @@ -242,7 +202,7 @@ class QuotedMessageWidget extends StatelessWidget { Map get _defaultAttachmentBuilder => { - 'image': (_, attachment) => ImageAttachment( + 'image': (_, attachment) => StreamImageAttachment( attachment: attachment, message: message, messageTheme: messageTheme, @@ -288,3 +248,47 @@ class QuotedMessageWidget extends StatelessWidget { return messageTheme.messageBackgroundColor; } } + +class _VideoAttachmentThumbnail extends StatefulWidget { + const _VideoAttachmentThumbnail({ + Key? key, + required this.attachment, + this.size = const Size(32, 32), + }) : super(key: key); + + final Size size; + final Attachment attachment; + + @override + _VideoAttachmentThumbnailState createState() => + _VideoAttachmentThumbnailState(); +} + +class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + _controller = VideoPlayerController.network(widget.attachment.assetUrl!) + ..initialize().then((_) { + // ignore: no-empty-block + setState(() {}); //when your thumbnail will show. + }); + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + @override + Widget build(BuildContext context) => SizedBox( + height: widget.size.height, + width: widget.size.width, + child: _controller.value.isInitialized + ? VideoPlayer(_controller) + : const CircularProgressIndicator(), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index dd3d5e27..99585001 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -4,10 +4,16 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro reaction_bubble} +@Deprecated("Use 'StreamReactionBubble' instead") +typedef ReactionBubble = StreamReactionBubble; + +/// {@template reaction_bubble} /// Creates reaction bubble widget for displaying over messages -class ReactionBubble extends StatelessWidget { - /// Constructor for creating a [ReactionBubble] - const ReactionBubble({ +/// {@endtemplate} +class StreamReactionBubble extends StatelessWidget { + /// Constructor for creating a [StreamReactionBubble] + const StreamReactionBubble({ Key? key, required this.reactions, required this.borderColor, @@ -111,7 +117,7 @@ class ReactionBubble extends StatelessWidget { } Widget _buildReaction( - List reactionIcons, + List reactionIcons, Reaction reaction, BuildContext context, ) { diff --git a/packages/stream_chat_flutter/lib/src/reaction_icon.dart b/packages/stream_chat_flutter/lib/src/reaction_icon.dart index e675128b..cf2cef81 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_icon.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_icon.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; /// Reaction icon data -class ReactionIcon { - /// Constructor for creating [ReactionIcon] - ReactionIcon({ +@Deprecated("Use 'StreamReactionIcon' instead") +typedef ReactionIcon = StreamReactionIcon; + +/// Reaction icon data +class StreamReactionIcon { + /// Constructor for creating [StreamReactionIcon] + StreamReactionIcon({ required this.type, required this.builder, }); diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reaction_picker.dart index 2991a1bb..28053c01 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_picker.dart @@ -3,16 +3,22 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro reaction_picker} +@Deprecated("Use 'StreamReactionPicker' instead") +typedef ReactionPicker = StreamReactionPicker; + +/// {@template reaction_picker} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.png) /// /// It shows a reaction picker /// /// Usually you don't use this widget as it's one of the default widgets used -/// by [MessageWidget.onMessageActions]. -class ReactionPicker extends StatefulWidget { - /// Constructor for creating a [ReactionPicker] widget - const ReactionPicker({ +/// by [StreamMessageWidget.onMessageActions]. +/// {@endtemplate} +class StreamReactionPicker extends StatefulWidget { + /// Constructor for creating a [StreamReactionPicker] widget + const StreamReactionPicker({ Key? key, required this.message, }) : super(key: key); @@ -21,10 +27,10 @@ class ReactionPicker extends StatefulWidget { final Message message; @override - _ReactionPickerState createState() => _ReactionPickerState(); + _StreamReactionPickerState createState() => _StreamReactionPickerState(); } -class _ReactionPickerState extends State +class _StreamReactionPickerState extends State with TickerProviderStateMixin { List animations = []; diff --git a/packages/stream_chat_flutter/lib/src/sending_indicator.dart b/packages/stream_chat_flutter/lib/src/sending_indicator.dart index 7f44e5b8..908c734d 100644 --- a/packages/stream_chat_flutter/lib/src/sending_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/sending_indicator.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro sending_indicator} +@Deprecated("Use 'StreamSendingIndicator' instead") +typedef SendingIndicator = StreamSendingIndicator; + +/// {@template sending_indicator} /// Used to show the sending status of the message -class SendingIndicator extends StatelessWidget { - /// Constructor for creating a [SendingIndicator] widget - const SendingIndicator({ +/// {@endtemplate} +class StreamSendingIndicator extends StatelessWidget { + /// Constructor for creating a [StreamSendingIndicator] widget + const StreamSendingIndicator({ Key? key, required this.message, this.isMessageRead = false, 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 84006f44..32b4830f 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -38,29 +38,29 @@ class StreamChatThemeData { /// Create a theme from scratch factory StreamChatThemeData({ Brightness? brightness, - TextTheme? textTheme, - ColorTheme? colorTheme, - ChannelListHeaderThemeData? channelListHeaderTheme, - ChannelPreviewThemeData? channelPreviewTheme, - ChannelHeaderThemeData? channelHeaderTheme, - MessageThemeData? otherMessageTheme, - MessageThemeData? ownMessageTheme, - MessageInputThemeData? messageInputTheme, + StreamTextTheme? textTheme, + StreamColorTheme? colorTheme, + StreamChannelListHeaderThemeData? channelListHeaderTheme, + StreamChannelPreviewThemeData? channelPreviewTheme, + StreamChannelHeaderThemeData? channelHeaderTheme, + StreamMessageThemeData? otherMessageTheme, + StreamMessageThemeData? ownMessageTheme, + StreamMessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, Widget Function(BuildContext, User)? placeholderUserImage, IconThemeData? primaryIconTheme, - List? reactionIcons, - GalleryHeaderThemeData? imageHeaderTheme, - GalleryFooterThemeData? imageFooterTheme, - MessageListViewThemeData? messageListViewTheme, - ChannelListViewThemeData? channelListViewTheme, - UserListViewThemeData? userListViewTheme, - MessageSearchListViewThemeData? messageSearchListViewTheme, + List? reactionIcons, + StreamGalleryHeaderThemeData? imageHeaderTheme, + StreamGalleryFooterThemeData? imageFooterTheme, + StreamMessageListViewThemeData? messageListViewTheme, + StreamChannelListViewThemeData? channelListViewTheme, + StreamUserListViewThemeData? userListViewTheme, + StreamMessageSearchListViewThemeData? messageSearchListViewTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; final isDark = brightness == Brightness.dark; - textTheme ??= isDark ? TextTheme.dark() : TextTheme.light(); - colorTheme ??= isDark ? ColorTheme.dark() : ColorTheme.light(); + textTheme ??= isDark ? StreamTextTheme.dark() : StreamTextTheme.light(); + colorTheme ??= isDark ? StreamColorTheme.dark() : StreamColorTheme.light(); final defaultData = StreamChatThemeData.fromColorAndTextTheme( colorTheme, @@ -133,14 +133,14 @@ class StreamChatThemeData { /// Create theme from color and text theme factory StreamChatThemeData.fromColorAndTextTheme( - ColorTheme colorTheme, - TextTheme textTheme, + StreamColorTheme colorTheme, + StreamTextTheme textTheme, ) { final accentColor = colorTheme.accentPrimary; final iconTheme = IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5)); - final channelHeaderTheme = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( + final channelHeaderTheme = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -153,9 +153,9 @@ class StreamChatThemeData { color: const Color(0xff7A7A7A), ), ); - final channelPreviewTheme = ChannelPreviewThemeData( + final channelPreviewTheme = StreamChannelPreviewThemeData( unreadCounterColor: colorTheme.accentError, - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -176,14 +176,14 @@ class StreamChatThemeData { colorTheme: colorTheme, primaryIconTheme: iconTheme, defaultUserImage: (context, user) => Center( - child: GradientAvatar( + child: StreamGradientAvatar( name: user.name, userId: user.id, ), ), channelPreviewTheme: channelPreviewTheme, - channelListHeaderTheme: ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( + channelListHeaderTheme: StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -194,7 +194,7 @@ class StreamChatThemeData { titleStyle: textTheme.headlineBold, ), channelHeaderTheme: channelHeaderTheme, - ownMessageTheme: MessageThemeData( + ownMessageTheme: StreamMessageThemeData( messageAuthorStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), messageTextStyle: textTheme.body, @@ -206,7 +206,7 @@ class StreamChatThemeData { reactionsBorderColor: colorTheme.borders, reactionsMaskColor: colorTheme.appBg, messageBorderColor: colorTheme.disabled, - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -218,7 +218,7 @@ class StreamChatThemeData { ), linkBackgroundColor: colorTheme.linkBg, ), - otherMessageTheme: MessageThemeData( + otherMessageTheme: StreamMessageThemeData( reactionsBackgroundColor: colorTheme.disabled, reactionsBorderColor: colorTheme.barsBg, reactionsMaskColor: colorTheme.appBg, @@ -233,7 +233,7 @@ class StreamChatThemeData { ), messageBackgroundColor: colorTheme.barsBg, messageBorderColor: colorTheme.borders, - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -242,7 +242,7 @@ class StreamChatThemeData { ), linkBackgroundColor: colorTheme.linkBg, ), - messageInputTheme: MessageInputThemeData( + messageInputTheme: StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), actionButtonColor: colorTheme.accentPrimary, @@ -267,7 +267,7 @@ class StreamChatThemeData { ), ), reactionIcons: [ - ReactionIcon( + StreamReactionIcon( type: 'love', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -279,7 +279,7 @@ class StreamChatThemeData { ); }, ), - ReactionIcon( + StreamReactionIcon( type: 'like', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -291,7 +291,7 @@ class StreamChatThemeData { ); }, ), - ReactionIcon( + StreamReactionIcon( type: 'sad', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -303,7 +303,7 @@ class StreamChatThemeData { ); }, ), - ReactionIcon( + StreamReactionIcon( type: 'haha', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -315,7 +315,7 @@ class StreamChatThemeData { ); }, ), - ReactionIcon( + StreamReactionIcon( type: 'wow', builder: (context, highlighted, size) { final theme = StreamChatTheme.of(context); @@ -328,7 +328,7 @@ class StreamChatThemeData { }, ), ], - galleryHeaderTheme: GalleryHeaderThemeData( + galleryHeaderTheme: StreamGalleryHeaderThemeData( closeButtonColor: colorTheme.textHighEmphasis, backgroundColor: channelHeaderTheme.color, iconMenuPointColor: colorTheme.textHighEmphasis, @@ -336,7 +336,7 @@ class StreamChatThemeData { subtitleTextStyle: channelPreviewTheme.subtitleStyle, bottomSheetBarrierColor: colorTheme.overlay, ), - galleryFooterTheme: GalleryFooterThemeData( + galleryFooterTheme: StreamGalleryFooterThemeData( backgroundColor: colorTheme.barsBg, shareIconColor: colorTheme.textHighEmphasis, titleTextStyle: textTheme.headlineBold, @@ -346,52 +346,52 @@ class StreamChatThemeData { bottomSheetPhotosTextStyle: textTheme.headlineBold, bottomSheetCloseIconColor: colorTheme.textHighEmphasis, ), - messageListViewTheme: MessageListViewThemeData( + messageListViewTheme: StreamMessageListViewThemeData( backgroundColor: colorTheme.barsBg, ), - channelListViewTheme: ChannelListViewThemeData( + channelListViewTheme: StreamChannelListViewThemeData( backgroundColor: colorTheme.appBg, ), - userListViewTheme: UserListViewThemeData( + userListViewTheme: StreamUserListViewThemeData( backgroundColor: colorTheme.appBg, ), - messageSearchListViewTheme: MessageSearchListViewThemeData( + messageSearchListViewTheme: StreamMessageSearchListViewThemeData( backgroundColor: colorTheme.appBg, ), ); } /// The text themes used in the widgets - final TextTheme textTheme; + final StreamTextTheme textTheme; /// The color themes used in the widgets - final ColorTheme colorTheme; + final StreamColorTheme colorTheme; - /// Theme of the [ChannelPreview] - final ChannelPreviewThemeData channelPreviewTheme; + /// Theme of the [StreamChannelPreview] + final StreamChannelPreviewThemeData channelPreviewTheme; - /// Theme of the [ChannelListHeader] - final ChannelListHeaderThemeData channelListHeaderTheme; + /// Theme of the [StreamChannelListHeader] + final StreamChannelListHeaderThemeData channelListHeaderTheme; /// Theme of the chat widgets dedicated to a channel header - final ChannelHeaderThemeData channelHeaderTheme; + final StreamChannelHeaderThemeData channelHeaderTheme; - /// The default style for [GalleryHeader]s below the overall + /// The default style for [StreamGalleryHeader]s below the overall /// [StreamChatTheme]. - final GalleryHeaderThemeData galleryHeaderTheme; + final StreamGalleryHeaderThemeData galleryHeaderTheme; - /// The default style for [GalleryFooter]s below the overall + /// The default style for [StreamGalleryFooter]s below the overall /// [StreamChatTheme]. - final GalleryFooterThemeData galleryFooterTheme; + final StreamGalleryFooterThemeData galleryFooterTheme; /// Theme of the current user messages - final MessageThemeData ownMessageTheme; + final StreamMessageThemeData ownMessageTheme; /// Theme of other users messages - final MessageThemeData otherMessageTheme; + final StreamMessageThemeData otherMessageTheme; - /// Theme dedicated to the [MessageInput] widget - final MessageInputThemeData messageInputTheme; + /// Theme dedicated to the [StreamMessageInput] widget + final StreamMessageInputThemeData messageInputTheme; /// The widget that will be built when the user image is unavailable final Widget Function(BuildContext, User) defaultUserImage; @@ -403,41 +403,41 @@ class StreamChatThemeData { final IconThemeData primaryIconTheme; /// Assets used for rendering reactions - final List reactionIcons; + final List reactionIcons; - /// Theme configuration for the [MessageListView] widget. - final MessageListViewThemeData messageListViewTheme; + /// Theme configuration for the [StreamMessageListView] widget. + final StreamMessageListViewThemeData messageListViewTheme; - /// Theme configuration for the [ChannelListView] widget. - final ChannelListViewThemeData channelListViewTheme; + /// Theme configuration for the [StreamChannelListView] widget. + final StreamChannelListViewThemeData channelListViewTheme; - /// Theme configuration for the [UserListView] widget. - final UserListViewThemeData userListViewTheme; + /// Theme configuration for the [StreamUserListView] widget. + final StreamUserListViewThemeData userListViewTheme; - /// Theme configuration for the [MessageSearchListView] widget. - final MessageSearchListViewThemeData messageSearchListViewTheme; + /// Theme configuration for the [StreamMessageSearchListView] widget. + final StreamMessageSearchListViewThemeData messageSearchListViewTheme; /// Creates a copy of [StreamChatThemeData] with specified attributes /// overridden. StreamChatThemeData copyWith({ - TextTheme? textTheme, - ColorTheme? colorTheme, - ChannelPreviewThemeData? channelPreviewTheme, - ChannelHeaderThemeData? channelHeaderTheme, - MessageThemeData? ownMessageTheme, - MessageThemeData? otherMessageTheme, - MessageInputThemeData? messageInputTheme, + StreamTextTheme? textTheme, + StreamColorTheme? colorTheme, + StreamChannelPreviewThemeData? channelPreviewTheme, + StreamChannelHeaderThemeData? channelHeaderTheme, + StreamMessageThemeData? ownMessageTheme, + StreamMessageThemeData? otherMessageTheme, + StreamMessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, Widget Function(BuildContext, User)? placeholderUserImage, IconThemeData? primaryIconTheme, - ChannelListHeaderThemeData? channelListHeaderTheme, - List? reactionIcons, - GalleryHeaderThemeData? galleryHeaderTheme, - GalleryFooterThemeData? galleryFooterTheme, - MessageListViewThemeData? messageListViewTheme, - ChannelListViewThemeData? channelListViewTheme, - UserListViewThemeData? userListViewTheme, - MessageSearchListViewThemeData? messageSearchListViewTheme, + StreamChannelListHeaderThemeData? channelListHeaderTheme, + List? reactionIcons, + StreamGalleryHeaderThemeData? galleryHeaderTheme, + StreamGalleryFooterThemeData? galleryFooterTheme, + StreamMessageListViewThemeData? messageListViewTheme, + StreamChannelListViewThemeData? channelListViewTheme, + StreamUserListViewThemeData? userListViewTheme, + StreamMessageSearchListViewThemeData? messageSearchListViewTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: diff --git a/packages/stream_chat_flutter/lib/src/system_message.dart b/packages/stream_chat_flutter/lib/src/system_message.dart index 297810b3..4ec10aa4 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/system_message.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// It shows a date divider depending on the date difference -class SystemMessage extends StatelessWidget { - /// Constructor for creating a [SystemMessage] - const SystemMessage({ +/// {@macro system_message} +@Deprecated("Use 'StreamSystemMessage' instead") +typedef SystemMessage = StreamSystemMessage; + +/// {@template system_message} +/// It shows a widget for the message with a system message type. +/// {@endtemplate} +class StreamSystemMessage extends StatelessWidget { + /// Constructor for creating a [StreamSystemMessage] + const StreamSystemMessage({ Key? key, required this.message, this.onMessageTap, diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart index 738fa0de..eb2d79bf 100644 --- a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -1,11 +1,17 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +/// {@macro avatar_theme_data} +@Deprecated("Use 'StreamAvatarThemeData' instead") +typedef AvatarThemeData = StreamAvatarThemeData; + +/// {@template avatar_theme_data} /// A style that overrides the default appearance of various avatar widgets. +/// {@endtemplate} // ignore: prefer-match-file-name -class AvatarThemeData with Diagnosticable { - /// Creates an [AvatarThemeData]. - const AvatarThemeData({ +class StreamAvatarThemeData with Diagnosticable { + /// Creates an [StreamAvatarThemeData]. + const StreamAvatarThemeData({ BoxConstraints? constraints, BorderRadius? borderRadius, }) : _constraints = constraints, @@ -25,12 +31,12 @@ class AvatarThemeData with Diagnosticable { /// Get border radius BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20); - /// Copy this [AvatarThemeData] to another. - AvatarThemeData copyWith({ + /// Copy this [StreamAvatarThemeData] to another. + StreamAvatarThemeData copyWith({ BoxConstraints? constraints, BorderRadius? borderRadius, }) => - AvatarThemeData( + StreamAvatarThemeData( constraints: constraints ?? _constraints, borderRadius: borderRadius ?? _borderRadius, ); @@ -38,12 +44,12 @@ class AvatarThemeData with Diagnosticable { /// Linearly interpolate between two [UserAvatar] themes. /// /// All the properties must be non-null. - AvatarThemeData lerp( - AvatarThemeData a, - AvatarThemeData b, + StreamAvatarThemeData lerp( + StreamAvatarThemeData a, + StreamAvatarThemeData b, double t, ) => - AvatarThemeData( + StreamAvatarThemeData( borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), constraints: BoxConstraints.lerp(a.constraints, b.constraints, t), ); @@ -51,7 +57,7 @@ class AvatarThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is AvatarThemeData && + other is StreamAvatarThemeData && runtimeType == other.runtimeType && _constraints == other._constraints && _borderRadius == other._borderRadius; @@ -59,8 +65,8 @@ class AvatarThemeData with Diagnosticable { @override int get hashCode => _constraints.hashCode ^ _borderRadius.hashCode; - /// Merges one [AvatarThemeData] with the another - AvatarThemeData merge(AvatarThemeData? other) { + /// Merges one [StreamAvatarThemeData] with the another + StreamAvatarThemeData merge(StreamAvatarThemeData? other) { if (other == null) return this; return copyWith( constraints: other._constraints, diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart index 63e208e5..aa988c0f 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -4,27 +4,33 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; +/// {@macro channel_header_theme} +@Deprecated("Use 'StreamChannelHeaderTheme' instead") +typedef ChannelHeaderTheme = StreamChannelHeaderTheme; + +/// {@template channel_header_theme} /// Overrides the default style of [ChannelHeader] descendants. /// /// See also: /// -/// * [ChannelHeaderThemeData], which is used to configure this theme. -class ChannelHeaderTheme extends InheritedTheme { - /// Creates a [ChannelHeaderTheme]. +/// * [StreamChannelHeaderThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamChannelHeaderTheme extends InheritedTheme { + /// Creates a [StreamChannelHeaderTheme]. /// /// The [data] parameter must not be null. - const ChannelHeaderTheme({ + const StreamChannelHeaderTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final ChannelHeaderThemeData data; + final StreamChannelHeaderThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [ChannelHeaderTheme] widget, then + /// If there is no enclosing [StreamChannelHeaderTheme] widget, then /// [StreamChatThemeData.channelTheme.channelHeaderTheme] is used. /// /// Typical usage is as follows: @@ -32,34 +38,40 @@ class ChannelHeaderTheme extends InheritedTheme { /// ```dart /// final theme = ChannelHeaderTheme.of(context); /// ``` - static ChannelHeaderThemeData of(BuildContext context) { + static StreamChannelHeaderThemeData of(BuildContext context) { final channelHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return channelHeaderTheme?.data ?? StreamChatTheme.of(context).channelHeaderTheme; } @override Widget wrap(BuildContext context, Widget child) => - ChannelHeaderTheme(data: data, child: child); + StreamChannelHeaderTheme(data: data, child: child); @override - bool updateShouldNotify(ChannelHeaderTheme oldWidget) => + bool updateShouldNotify(StreamChannelHeaderTheme oldWidget) => data != oldWidget.data; } +/// {@macro channel_header_theme_data} +@Deprecated("Use 'StreamChannelHeaderThemeData' instead") +typedef ChannelHeaderThemeData = StreamChannelHeaderThemeData; + +/// {@template channel_header_theme_data} /// A style that overrides the default appearance of [ChannelHeader]s when used -/// with [ChannelHeaderTheme] or with the overall [StreamChatTheme]'s +/// with [StreamChannelHeaderTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.channelHeaderTheme]. /// /// See also: /// -/// * [ChannelHeaderTheme], the theme which is configured with this class. +/// * [StreamChannelHeaderTheme], the theme which is configured with this class. /// * [StreamChatThemeData.channelHeaderTheme], which can be used to override /// the default style for [ChannelHeader]s below the overall [StreamChatTheme]. -class ChannelHeaderThemeData with Diagnosticable { - /// Creates a [ChannelHeaderThemeData] - const ChannelHeaderThemeData({ +/// {@endtemplate} +class StreamChannelHeaderThemeData with Diagnosticable { + /// Creates a [StreamChannelHeaderThemeData] + const StreamChannelHeaderThemeData({ this.titleStyle, this.subtitleStyle, this.avatarTheme, @@ -73,43 +85,43 @@ class ChannelHeaderThemeData with Diagnosticable { final TextStyle? subtitleStyle; /// Theme for avatar - final AvatarThemeData? avatarTheme; + final StreamAvatarThemeData? avatarTheme; - /// Color for [ChannelHeaderThemeData] + /// Color for [StreamChannelHeaderThemeData] final Color? color; /// Copy with theme - ChannelHeaderThemeData copyWith({ + StreamChannelHeaderThemeData copyWith({ TextStyle? titleStyle, TextStyle? subtitleStyle, - AvatarThemeData? avatarTheme, + StreamAvatarThemeData? avatarTheme, Color? color, }) => - ChannelHeaderThemeData( + StreamChannelHeaderThemeData( titleStyle: titleStyle ?? this.titleStyle, subtitleStyle: subtitleStyle ?? this.subtitleStyle, avatarTheme: avatarTheme ?? this.avatarTheme, color: color ?? this.color, ); - /// Linearly interpolate between two [ChannelHeaderThemeData]. + /// Linearly interpolate between two [StreamChannelHeaderThemeData]. /// /// All the properties must be non-null. - ChannelHeaderThemeData lerp( - ChannelHeaderThemeData a, - ChannelHeaderThemeData b, + StreamChannelHeaderThemeData lerp( + StreamChannelHeaderThemeData a, + StreamChannelHeaderThemeData b, double t, ) => - ChannelHeaderThemeData( + StreamChannelHeaderThemeData( titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), - avatarTheme: - const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + avatarTheme: const StreamAvatarThemeData() + .lerp(a.avatarTheme!, b.avatarTheme!, t), color: Color.lerp(a.color, b.color, t), ); - /// Merge with other [ChannelHeaderThemeData] - ChannelHeaderThemeData merge(ChannelHeaderThemeData? other) { + /// Merge with other [StreamChannelHeaderThemeData] + StreamChannelHeaderThemeData merge(StreamChannelHeaderThemeData? other) { if (other == null) return this; return copyWith( titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, @@ -123,7 +135,7 @@ class ChannelHeaderThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is ChannelHeaderThemeData && + other is StreamChannelHeaderThemeData && runtimeType == other.runtimeType && titleStyle == other.titleStyle && subtitleStyle == other.subtitleStyle && diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart index 3846232e..7cff2b90 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -3,27 +3,33 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +/// {@macro channel_list_header_theme} +@Deprecated("Use 'StreamChannelListHeaderTheme' instead") +typedef ChannelListHeaderTheme = StreamChannelListHeaderTheme; + +/// {@template channel_list_header_theme} /// Overrides the default style of [ChannelListHeader] descendants. /// /// See also: /// -/// * [ChannelListHeaderThemeData], which is used to configure this theme. -class ChannelListHeaderTheme extends InheritedTheme { - /// Creates a [ChannelListHeaderTheme]. +/// * [StreamChannelListHeaderThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamChannelListHeaderTheme extends InheritedTheme { + /// Creates a [StreamChannelListHeaderTheme]. /// /// The [data] parameter must not be null. - const ChannelListHeaderTheme({ + const StreamChannelListHeaderTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final ChannelListHeaderThemeData data; + final StreamChannelListHeaderThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [ChannelListHeaderTheme] widget, then + /// If there is no enclosing [StreamChannelListHeaderTheme] widget, then /// [StreamChatThemeData.channelListHeaderTheme] is used. /// /// Typical usage is as follows: @@ -31,26 +37,32 @@ class ChannelListHeaderTheme extends InheritedTheme { /// ```dart /// final theme = ChannelListHeaderTheme.of(context); /// ``` - static ChannelListHeaderThemeData of(BuildContext context) { - final channelListHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); + static StreamChannelListHeaderThemeData of(BuildContext context) { + final channelListHeaderTheme = context + .dependOnInheritedWidgetOfExactType(); return channelListHeaderTheme?.data ?? StreamChatTheme.of(context).channelListHeaderTheme; } @override Widget wrap(BuildContext context, Widget child) => - ChannelListHeaderTheme(data: data, child: child); + StreamChannelListHeaderTheme(data: data, child: child); @override - bool updateShouldNotify(ChannelListHeaderTheme oldWidget) => + bool updateShouldNotify(StreamChannelListHeaderTheme oldWidget) => data != oldWidget.data; } +/// {@macro channel_list_header_theme_data} +@Deprecated("Use ''StreamChannelListHeaderThemeData' instead") +typedef ChannelListHeaderThemeData = StreamChannelListHeaderThemeData; + +/// {@template channel_list_header_theme_data} /// Theme dedicated to the [ChannelListHeader] -class ChannelListHeaderThemeData with Diagnosticable { - /// Returns a new [ChannelListHeaderThemeData] - const ChannelListHeaderThemeData({ +/// {@endtemplate} +class StreamChannelListHeaderThemeData with Diagnosticable { + /// Returns a new [StreamChannelListHeaderThemeData] + const StreamChannelListHeaderThemeData({ this.titleStyle, this.avatarTheme, this.color, @@ -60,39 +72,41 @@ class ChannelListHeaderThemeData with Diagnosticable { final TextStyle? titleStyle; /// Theme dedicated to the userAvatar - final AvatarThemeData? avatarTheme; + final StreamAvatarThemeData? avatarTheme; /// Background color of the appbar final Color? color; - /// Returns a new [ChannelListHeaderThemeData] replacing some of its + /// Returns a new [StreamChannelListHeaderThemeData] replacing some of its /// properties - ChannelListHeaderThemeData copyWith({ + StreamChannelListHeaderThemeData copyWith({ TextStyle? titleStyle, - AvatarThemeData? avatarTheme, + StreamAvatarThemeData? avatarTheme, Color? color, }) => - ChannelListHeaderThemeData( + StreamChannelListHeaderThemeData( titleStyle: titleStyle ?? this.titleStyle, avatarTheme: avatarTheme ?? this.avatarTheme, color: color ?? this.color, ); - /// Linearly interpolate from one [ChannelListHeaderThemeData] to another. - ChannelListHeaderThemeData lerp( - ChannelListHeaderThemeData a, - ChannelListHeaderThemeData b, + /// Linearly interpolate from one [StreamChannelListHeaderThemeData] to another. + StreamChannelListHeaderThemeData lerp( + StreamChannelListHeaderThemeData a, + StreamChannelListHeaderThemeData b, double t, ) => - ChannelListHeaderThemeData( - avatarTheme: - const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + StreamChannelListHeaderThemeData( + avatarTheme: const StreamAvatarThemeData() + .lerp(a.avatarTheme!, b.avatarTheme!, t), color: Color.lerp(a.color, b.color, t), titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), ); - /// Merges [this] [ChannelListHeaderThemeData] with the [other] - ChannelListHeaderThemeData merge(ChannelListHeaderThemeData? other) { + /// Merges [this] [StreamChannelListHeaderThemeData] with the [other] + StreamChannelListHeaderThemeData merge( + StreamChannelListHeaderThemeData? other, + ) { if (other == null) return this; return copyWith( titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, @@ -104,7 +118,7 @@ class ChannelListHeaderThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is ChannelListHeaderThemeData && + other is StreamChannelListHeaderThemeData && runtimeType == other.runtimeType && titleStyle == other.titleStyle && avatarTheme == other.avatarTheme && diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart index d310e2bc..4d6bd7d0 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro channel_list_view_theme} +@Deprecated("Use 'StreamChannelListViewTheme' instead") +typedef ChannelListViewTheme = StreamChannelListViewTheme; + +/// {@template channel_list_view_theme} /// Overrides the default style of [ChannelListView] descendants. /// /// See also: /// -/// * [ChannelListViewThemeData], which is used to configure this theme. -class ChannelListViewTheme extends InheritedTheme { - /// Creates a [ChannelListViewTheme]. +/// * [StreamChannelListViewThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamChannelListViewTheme extends InheritedTheme { + /// Creates a [StreamChannelListViewTheme]. /// /// The [data] parameter must not be null. - const ChannelListViewTheme({ + const StreamChannelListViewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final ChannelListViewThemeData data; + final StreamChannelListViewThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [ChannelListViewTheme] widget, then + /// If there is no enclosing [StreamChannelListViewTheme] widget, then /// [StreamChatThemeData.channelListViewTheme] is used. /// /// Typical usage is as follows: @@ -30,63 +36,69 @@ class ChannelListViewTheme extends InheritedTheme { /// ```dart /// ChannelListViewTheme theme = ChannelListViewTheme.of(context); /// ``` - static ChannelListViewThemeData of(BuildContext context) { - final channelListViewTheme = - context.dependOnInheritedWidgetOfExactType(); + static StreamChannelListViewThemeData of(BuildContext context) { + final channelListViewTheme = context + .dependOnInheritedWidgetOfExactType(); return channelListViewTheme?.data ?? StreamChatTheme.of(context).channelListViewTheme; } @override Widget wrap(BuildContext context, Widget child) => - ChannelListViewTheme(data: data, child: child); + StreamChannelListViewTheme(data: data, child: child); @override - bool updateShouldNotify(ChannelListViewTheme oldWidget) => + bool updateShouldNotify(StreamChannelListViewTheme oldWidget) => data != oldWidget.data; } +/// {@macro channel_list_view_theme_data} +@Deprecated("Use 'StreamChannelListViewThemeData' instead") +typedef ChannelListViewThemeData = StreamChannelListViewThemeData; + +/// {@template channel_list_view_theme_data} /// A style that overrides the default appearance of [ChannelListView]s when -/// used with [ChannelListViewTheme] or with the overall [StreamChatTheme]'s +/// used with [StreamChannelListViewTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.channelListViewTheme]. /// /// See also: /// -/// * [ChannelListViewTheme], the theme which is configured with this class. +/// * [StreamChannelListViewTheme], the theme which is configured with this class. /// * [StreamChatThemeData.channelListViewTheme], which can be used to override /// the default style for [ChannelListView]s below the overall /// [StreamChatTheme]. -class ChannelListViewThemeData with Diagnosticable { - /// Creates a [ChannelListViewThemeData]. - const ChannelListViewThemeData({ +/// {@endtemplate} +class StreamChannelListViewThemeData with Diagnosticable { + /// Creates a [StreamChannelListViewThemeData]. + const StreamChannelListViewThemeData({ this.backgroundColor, }); /// The color of the [ChannelListView] background. final Color? backgroundColor; - /// Copies this [ChannelListViewThemeData] to another. - ChannelListViewThemeData copyWith({ + /// Copies this [StreamChannelListViewThemeData] to another. + StreamChannelListViewThemeData copyWith({ Color? backgroundColor, }) => - ChannelListViewThemeData( + StreamChannelListViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, ); - /// Linearly interpolate between two [ChannelListViewThemeData] themes. + /// Linearly interpolate between two [StreamChannelListViewThemeData] themes. /// /// All the properties must be non-null. - ChannelListViewThemeData lerp( - ChannelListViewThemeData a, - ChannelListViewThemeData b, + StreamChannelListViewThemeData lerp( + StreamChannelListViewThemeData a, + StreamChannelListViewThemeData b, double t, ) => - ChannelListViewThemeData( + StreamChannelListViewThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), ); - /// Merges one [ChannelListViewThemeData] with another. - ChannelListViewThemeData merge(ChannelListViewThemeData? other) { + /// Merges one [StreamChannelListViewThemeData] with another. + StreamChannelListViewThemeData merge(StreamChannelListViewThemeData? other) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -96,7 +108,7 @@ class ChannelListViewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is ChannelListViewThemeData && + other is StreamChannelListViewThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor; diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index ff9ef70c..3df974b2 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -3,27 +3,33 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +/// {@macro channel_preview_theme} +@Deprecated("Use 'StreamChannelPreviewTheme' instead") +typedef ChannelPreviewTheme = StreamChannelPreviewTheme; + +/// {@template channel_preview_theme} /// Overrides the default style of [ChannelPreview] descendants. /// /// See also: /// -/// * [ChannelPreviewThemeData], which is used to configure this theme. -class ChannelPreviewTheme extends InheritedTheme { - /// Creates a [ChannelPreviewTheme]. +/// * [StreamChannelPreviewThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamChannelPreviewTheme extends InheritedTheme { + /// Creates a [StreamChannelPreviewTheme]. /// /// The [data] parameter must not be null. - const ChannelPreviewTheme({ + const StreamChannelPreviewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final ChannelPreviewThemeData data; + final StreamChannelPreviewThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [ChannelPreviewTheme] widget, then + /// If there is no enclosing [StreamChannelPreviewTheme] widget, then /// [StreamChatThemeData.channelPreviewTheme] is used. /// /// Typical usage is as follows: @@ -31,34 +37,40 @@ class ChannelPreviewTheme extends InheritedTheme { /// ```dart /// final theme = ChannelPreviewTheme.of(context); /// ``` - static ChannelPreviewThemeData of(BuildContext context) { + static StreamChannelPreviewThemeData of(BuildContext context) { final channelPreviewTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return channelPreviewTheme?.data ?? StreamChatTheme.of(context).channelPreviewTheme; } @override Widget wrap(BuildContext context, Widget child) => - ChannelPreviewTheme(data: data, child: child); + StreamChannelPreviewTheme(data: data, child: child); @override - bool updateShouldNotify(ChannelPreviewTheme oldWidget) => + bool updateShouldNotify(StreamChannelPreviewTheme oldWidget) => data != oldWidget.data; } +/// {@macro channel_preview_theme_data} +@Deprecated("Use 'StreamChannelPreviewThemeData' instead") +typedef ChannelPreviewThemeData = StreamChannelPreviewThemeData; + +/// {@template channel_preview_theme_data} /// A style that overrides the default appearance of [ChannelPreview]s when used -/// with [ChannelPreviewTheme] or with the overall [StreamChatTheme]'s +/// with [StreamChannelPreviewTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.channelPreviewTheme]. /// /// See also: /// -/// * [ChannelPreviewTheme], the theme which is configured with this class. +/// * [StreamChannelPreviewTheme], the theme which is configured with this class. /// * [StreamChatThemeData.channelPreviewTheme], which can be used to override /// the default style for [ChannelHeader]s below the overall [StreamChatTheme]. -class ChannelPreviewThemeData with Diagnosticable { - /// Creates a [ChannelPreviewThemeData]. - const ChannelPreviewThemeData({ +/// {@endtemplate} +class StreamChannelPreviewThemeData with Diagnosticable { + /// Creates a [StreamChannelPreviewThemeData]. + const StreamChannelPreviewThemeData({ this.titleStyle, this.subtitleStyle, this.lastMessageAtStyle, @@ -77,7 +89,7 @@ class ChannelPreviewThemeData with Diagnosticable { final TextStyle? lastMessageAtStyle; /// Avatar theme - final AvatarThemeData? avatarTheme; + final StreamAvatarThemeData? avatarTheme; /// Unread counter color final Color? unreadCounterColor; @@ -86,15 +98,15 @@ class ChannelPreviewThemeData with Diagnosticable { final double? indicatorIconSize; /// Copy with theme - ChannelPreviewThemeData copyWith({ + StreamChannelPreviewThemeData copyWith({ TextStyle? titleStyle, TextStyle? subtitleStyle, TextStyle? lastMessageAtStyle, - AvatarThemeData? avatarTheme, + StreamAvatarThemeData? avatarTheme, Color? unreadCounterColor, double? indicatorIconSize, }) => - ChannelPreviewThemeData( + StreamChannelPreviewThemeData( titleStyle: titleStyle ?? this.titleStyle, subtitleStyle: subtitleStyle ?? this.subtitleStyle, lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle, @@ -103,15 +115,15 @@ class ChannelPreviewThemeData with Diagnosticable { indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, ); - /// Linearly interpolate one [ChannelPreviewThemeData] to another. - ChannelPreviewThemeData lerp( - ChannelPreviewThemeData a, - ChannelPreviewThemeData b, + /// Linearly interpolate one [StreamChannelPreviewThemeData] to another. + StreamChannelPreviewThemeData lerp( + StreamChannelPreviewThemeData a, + StreamChannelPreviewThemeData b, double t, ) => - ChannelPreviewThemeData( - avatarTheme: - const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + StreamChannelPreviewThemeData( + avatarTheme: const StreamAvatarThemeData() + .lerp(a.avatarTheme!, b.avatarTheme!, t), indicatorIconSize: a.indicatorIconSize, lastMessageAtStyle: TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t), @@ -122,7 +134,7 @@ class ChannelPreviewThemeData with Diagnosticable { ); /// Merge with theme - ChannelPreviewThemeData merge(ChannelPreviewThemeData? other) { + StreamChannelPreviewThemeData merge(StreamChannelPreviewThemeData? other) { if (other == null) return this; return copyWith( titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, @@ -138,7 +150,7 @@ class ChannelPreviewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is ChannelPreviewThemeData && + other is StreamChannelPreviewThemeData && runtimeType == other.runtimeType && titleStyle == other.titleStyle && subtitleStyle == other.subtitleStyle && diff --git a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart index b32c023f..e0068d59 100644 --- a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; +/// {@macro color_theme} +@Deprecated("Use 'StreamColorTheme' instead") +typedef ColorTheme = StreamColorTheme; + +/// {@template color_theme} /// Theme that holds colors -class ColorTheme { +/// {@endtemplate} +class StreamColorTheme { /// Initialise with light theme - ColorTheme.light({ + StreamColorTheme.light({ this.textHighEmphasis = const Color(0xff000000), this.textLowEmphasis = const Color(0xff7a7a7a), this.disabled = const Color(0xffdbdbdb), @@ -55,7 +61,7 @@ class ColorTheme { }) : brightness = Brightness.light; /// Initialise with dark theme - ColorTheme.dark({ + StreamColorTheme.dark({ this.textHighEmphasis = const Color(0xffffffff), this.textLowEmphasis = const Color(0xff7a7a7a), this.disabled = const Color(0xff2d2f2f), @@ -169,7 +175,7 @@ class ColorTheme { final Brightness brightness; /// Copy with theme - ColorTheme copyWith({ + StreamColorTheme copyWith({ Brightness brightness = Brightness.light, Color? textHighEmphasis, Color? textLowEmphasis, @@ -192,7 +198,7 @@ class ColorTheme { Gradient? bgGradient, }) => brightness == Brightness.light - ? ColorTheme.light( + ? StreamColorTheme.light( textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, disabled: disabled ?? this.disabled, @@ -213,7 +219,7 @@ class ColorTheme { overlayDark: overlayDark ?? this.overlayDark, bgGradient: bgGradient ?? this.bgGradient, ) - : ColorTheme.dark( + : StreamColorTheme.dark( textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, disabled: disabled ?? this.disabled, @@ -236,7 +242,7 @@ class ColorTheme { ); /// Merge color theme - ColorTheme merge(ColorTheme? other) { + StreamColorTheme merge(StreamColorTheme? other) { if (other == null) return this; return copyWith( textHighEmphasis: other.textHighEmphasis, diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart index a227b830..f68ceb45 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro gallery_footer_theme} +@Deprecated("Use 'StreamGalleryFooterTheme' instead") +typedef GalleryFooterTheme = StreamGalleryFooterTheme; + +/// {@template gallery_footer_theme} /// Overrides the default style of [GalleryFooter] descendants. /// /// See also: /// -/// * [GalleryFooterThemeData], which is used to configure this theme. -class GalleryFooterTheme extends InheritedTheme { - /// Creates an [GalleryFooterTheme]. +/// * [StreamGalleryFooterThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamGalleryFooterTheme extends InheritedTheme { + /// Creates an [StreamGalleryFooterTheme]. /// /// The [data] parameter must not be null. - const GalleryFooterTheme({ + const StreamGalleryFooterTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final GalleryFooterThemeData data; + final StreamGalleryFooterThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [GalleryFooterTheme] widget, then + /// If there is no enclosing [StreamGalleryFooterTheme] widget, then /// [StreamChatThemeData.galleryFooterTheme] is used. /// /// Typical usage is as follows: @@ -30,34 +36,40 @@ class GalleryFooterTheme extends InheritedTheme { /// ```dart /// ImageFooterTheme theme = ImageFooterTheme.of(context); /// ``` - static GalleryFooterThemeData of(BuildContext context) { + static StreamGalleryFooterThemeData of(BuildContext context) { final imageFooterTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return imageFooterTheme?.data ?? StreamChatTheme.of(context).galleryFooterTheme; } @override Widget wrap(BuildContext context, Widget child) => - GalleryFooterTheme(data: data, child: child); + StreamGalleryFooterTheme(data: data, child: child); @override - bool updateShouldNotify(GalleryFooterTheme oldWidget) => + bool updateShouldNotify(StreamGalleryFooterTheme oldWidget) => data != oldWidget.data; } +/// {@macro gallery_footer_theme_data} +@Deprecated("Use 'StreamGalleryFooterThemeData' instead") +typedef GalleryFooterThemeData = StreamGalleryFooterThemeData; + +/// {@template gallery_footer_theme_data} /// A style that overrides the default appearance of [GalleryFooter]s when used -/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s +/// with [StreamGalleryFooterTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.galleryFooterTheme]. /// /// See also: /// -/// * [GalleryFooterTheme], the theme which is configured with this class. +/// * [StreamGalleryFooterTheme], the theme which is configured with this class. /// * [StreamChatThemeData.galleryFooterTheme], which can be used to override /// the default style for [GalleryFooter]s below the overall [StreamChatTheme]. -class GalleryFooterThemeData with Diagnosticable { - /// Creates an [GalleryFooterThemeData]. - const GalleryFooterThemeData({ +/// {@endtemplate} +class StreamGalleryFooterThemeData with Diagnosticable { + /// Creates an [StreamGalleryFooterThemeData]. + const StreamGalleryFooterThemeData({ this.backgroundColor, this.shareIconColor, this.titleTextStyle, @@ -108,8 +120,8 @@ class GalleryFooterThemeData with Diagnosticable { /// Defaults to [ColorTheme.textHighEmphasis]. final Color? bottomSheetCloseIconColor; - /// Copies this [GalleryFooterThemeData] to another. - GalleryFooterThemeData copyWith({ + /// Copies this [StreamGalleryFooterThemeData] to another. + StreamGalleryFooterThemeData copyWith({ Color? backgroundColor, Color? shareIconColor, TextStyle? titleTextStyle, @@ -119,7 +131,7 @@ class GalleryFooterThemeData with Diagnosticable { TextStyle? bottomSheetPhotosTextStyle, Color? bottomSheetCloseIconColor, }) => - GalleryFooterThemeData( + StreamGalleryFooterThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, shareIconColor: shareIconColor ?? this.shareIconColor, titleTextStyle: titleTextStyle ?? this.titleTextStyle, @@ -137,12 +149,12 @@ class GalleryFooterThemeData with Diagnosticable { /// Linearly interpolate between two [GalleryFooter] themes. /// /// All the properties must be non-null. - GalleryFooterThemeData lerp( - GalleryFooterThemeData a, - GalleryFooterThemeData b, + StreamGalleryFooterThemeData lerp( + StreamGalleryFooterThemeData a, + StreamGalleryFooterThemeData b, double t, ) => - GalleryFooterThemeData( + StreamGalleryFooterThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), @@ -167,8 +179,8 @@ class GalleryFooterThemeData with Diagnosticable { ), ); - /// Merges one [GalleryFooterThemeData] with another. - GalleryFooterThemeData merge(GalleryFooterThemeData? other) { + /// Merges one [StreamGalleryFooterThemeData] with another. + StreamGalleryFooterThemeData merge(StreamGalleryFooterThemeData? other) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -185,7 +197,7 @@ class GalleryFooterThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is GalleryFooterThemeData && + other is StreamGalleryFooterThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor && shareIconColor == other.shareIconColor && diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart index 23402c57..f03ebe9a 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro gallery_header_them} +@Deprecated("Use 'StreamGalleryHeaderTheme' instead") +typedef GalleryHeaderTheme = StreamGalleryHeaderTheme; + +/// {@template gallery_header_theme} /// Overrides the default style of [GalleryHeader] descendants. /// /// See also: /// -/// * [GalleryHeaderThemeData], which is used to configure this theme. -class GalleryHeaderTheme extends InheritedTheme { - /// Creates a [GalleryHeaderTheme]. +/// * [StreamGalleryHeaderThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamGalleryHeaderTheme extends InheritedTheme { + /// Creates a [StreamGalleryHeaderTheme]. /// /// The [data] parameter must not be null. - const GalleryHeaderTheme({ + const StreamGalleryHeaderTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final GalleryHeaderThemeData data; + final StreamGalleryHeaderThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [GalleryHeaderTheme] widget, then + /// If there is no enclosing [StreamGalleryHeaderTheme] widget, then /// [StreamChatThemeData.galleryHeaderTheme] is used. /// /// Typical usage is as follows: @@ -30,34 +36,40 @@ class GalleryHeaderTheme extends InheritedTheme { /// ```dart /// ImageHeaderTheme theme = ImageHeaderTheme.of(context); /// ``` - static GalleryHeaderThemeData of(BuildContext context) { + static StreamGalleryHeaderThemeData of(BuildContext context) { final galleryHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return galleryHeaderTheme?.data ?? StreamChatTheme.of(context).galleryHeaderTheme; } @override Widget wrap(BuildContext context, Widget child) => - GalleryHeaderTheme(data: data, child: child); + StreamGalleryHeaderTheme(data: data, child: child); @override - bool updateShouldNotify(GalleryHeaderTheme oldWidget) => + bool updateShouldNotify(StreamGalleryHeaderTheme oldWidget) => data != oldWidget.data; } +/// {@macro gallery_header_theme_data} +@Deprecated("Use 'StreamGalleryHeaderThemeData' instead") +typedef GalleryHeaderThemeData = StreamGalleryHeaderThemeData; + +/// {@template gallery_header_theme_data} /// A style that overrides the default appearance of [GalleryHeader]s when used -/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s +/// with [StreamGalleryHeaderTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.galleryHeaderTheme]. /// /// See also: /// -/// * [GalleryHeaderTheme], the theme which is configured with this class. +/// * [StreamGalleryHeaderTheme], the theme which is configured with this class. /// * [StreamChatThemeData.galleryHeaderTheme], which can be used to override /// the default style for [GalleryHeader]s below the overall [StreamChatTheme]. -class GalleryHeaderThemeData with Diagnosticable { - /// Creates an [GalleryHeaderThemeData]. - const GalleryHeaderThemeData({ +/// {@endtemplate} +class StreamGalleryHeaderThemeData with Diagnosticable { + /// Creates an [StreamGalleryHeaderThemeData]. + const StreamGalleryHeaderThemeData({ this.closeButtonColor, this.backgroundColor, this.iconMenuPointColor, @@ -92,8 +104,8 @@ class GalleryHeaderThemeData with Diagnosticable { /// final Color? bottomSheetBarrierColor; - /// Copies this [GalleryHeaderThemeData] to another. - GalleryHeaderThemeData copyWith({ + /// Copies this [StreamGalleryHeaderThemeData] to another. + StreamGalleryHeaderThemeData copyWith({ Color? closeButtonColor, Color? backgroundColor, Color? iconMenuPointColor, @@ -101,7 +113,7 @@ class GalleryHeaderThemeData with Diagnosticable { TextStyle? subtitleTextStyle, Color? bottomSheetBarrierColor, }) => - GalleryHeaderThemeData( + StreamGalleryHeaderThemeData( closeButtonColor: closeButtonColor ?? this.closeButtonColor, backgroundColor: backgroundColor ?? this.backgroundColor, iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, @@ -114,12 +126,12 @@ class GalleryHeaderThemeData with Diagnosticable { /// Linearly interpolate between two [GalleryHeader] themes. /// /// All the properties must be non-null. - GalleryHeaderThemeData lerp( - GalleryHeaderThemeData a, - GalleryHeaderThemeData b, + StreamGalleryHeaderThemeData lerp( + StreamGalleryHeaderThemeData a, + StreamGalleryHeaderThemeData b, double t, ) => - GalleryHeaderThemeData( + StreamGalleryHeaderThemeData( closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), iconMenuPointColor: @@ -131,8 +143,8 @@ class GalleryHeaderThemeData with Diagnosticable { Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), ); - /// Merges one [GalleryHeaderThemeData] with the another - GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) { + /// Merges one [StreamGalleryHeaderThemeData] with the another + StreamGalleryHeaderThemeData merge(StreamGalleryHeaderThemeData? other) { if (other == null) return this; return copyWith( closeButtonColor: other.closeButtonColor, @@ -147,7 +159,7 @@ class GalleryHeaderThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is GalleryHeaderThemeData && + other is StreamGalleryHeaderThemeData && runtimeType == other.runtimeType && closeButtonColor == other.closeButtonColor && backgroundColor == other.backgroundColor && 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 81047d77..c3874ef6 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 @@ -5,27 +5,33 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro message_input_theme} +@Deprecated("Use 'StreamMessageInputTheme' instead") +typedef MessageInputTheme = StreamMessageInputTheme; + +/// {@template message_input_theme} /// Overrides the default style of [MessageInput] descendants. /// /// See also: /// -/// * [MessageInputThemeData], which is used to configure this theme. -class MessageInputTheme extends InheritedTheme { - /// Creates a [MessageInputTheme]. +/// * [StreamMessageInputThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamMessageInputTheme extends InheritedTheme { + /// Creates a [StreamMessageInputTheme]. /// /// The [data] parameter must not be null. - const MessageInputTheme({ + const StreamMessageInputTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final MessageInputThemeData data; + final StreamMessageInputThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [MessageInputTheme] widget, then + /// If there is no enclosing [StreamMessageInputTheme] widget, then /// [StreamChatThemeData.messageInputTheme] is used. /// /// Typical usage is as follows: @@ -33,28 +39,34 @@ class MessageInputTheme extends InheritedTheme { /// ```dart /// final theme = MessageInputTheme.of(context); /// ``` - static MessageInputThemeData of(BuildContext context) { + static StreamMessageInputThemeData of(BuildContext context) { final messageInputTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return messageInputTheme?.data ?? StreamChatTheme.of(context).messageInputTheme; } @override Widget wrap(BuildContext context, Widget child) => - MessageInputTheme(data: data, child: child); + StreamMessageInputTheme(data: data, child: child); @override - bool updateShouldNotify(MessageInputTheme oldWidget) => + bool updateShouldNotify(StreamMessageInputTheme oldWidget) => data != oldWidget.data; } +/// {@macro message_input_theme_data} +@Deprecated("Use 'StreamMessageInputThemeData' instead") +typedef MessageInputThemeData = StreamMessageInputThemeData; + +/// {@template message_input_theme_data} /// A style that overrides the default appearance of [MessageInput] widgets -/// when used with [MessageInputTheme] or with the overall [StreamChatTheme]'s +/// when used with [StreamMessageInputTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.messageInputTheme]. -class MessageInputThemeData with Diagnosticable { - /// Creates a [MessageInputThemeData]. - const MessageInputThemeData({ +/// {@endtemplate} +class StreamMessageInputThemeData with Diagnosticable { + /// Creates a [StreamMessageInputThemeData]. + const StreamMessageInputThemeData({ this.sendAnimationDuration, this.actionButtonColor, this.sendButtonColor, @@ -121,8 +133,8 @@ class MessageInputThemeData with Diagnosticable { /// Shadow for the [MessageInput] widget final BoxShadow? shadow; - /// Returns a new [MessageInputThemeData] replacing some of its properties - MessageInputThemeData copyWith({ + /// Returns a new [StreamMessageInputThemeData] replacing some of its properties + StreamMessageInputThemeData copyWith({ Duration? sendAnimationDuration, Color? inputBackgroundColor, Color? actionButtonColor, @@ -140,7 +152,7 @@ class MessageInputThemeData with Diagnosticable { double? elevation, BoxShadow? shadow, }) => - MessageInputThemeData( + StreamMessageInputThemeData( sendAnimationDuration: sendAnimationDuration ?? this.sendAnimationDuration, inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor, @@ -161,13 +173,13 @@ class MessageInputThemeData with Diagnosticable { shadow: shadow ?? this.shadow, ); - /// Linearly interpolate from one [MessageInputThemeData] to another. - MessageInputThemeData lerp( - MessageInputThemeData a, - MessageInputThemeData b, + /// Linearly interpolate from one [StreamMessageInputThemeData] to another. + StreamMessageInputThemeData lerp( + StreamMessageInputThemeData a, + StreamMessageInputThemeData b, double t, ) => - MessageInputThemeData( + StreamMessageInputThemeData( actionButtonColor: Color.lerp(a.actionButtonColor, b.actionButtonColor, t), actionButtonIdleColor: @@ -194,8 +206,8 @@ class MessageInputThemeData with Diagnosticable { shadow: BoxShadow.lerp(a.shadow, b.shadow, t), ); - /// Merges [this] [MessageInputThemeData] with the [other] - MessageInputThemeData merge(MessageInputThemeData? other) { + /// Merges [this] [StreamMessageInputThemeData] with the [other] + StreamMessageInputThemeData merge(StreamMessageInputThemeData? other) { if (other == null) return this; return copyWith( sendAnimationDuration: other.sendAnimationDuration, @@ -222,7 +234,7 @@ class MessageInputThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is MessageInputThemeData && + other is StreamMessageInputThemeData && runtimeType == other.runtimeType && sendAnimationDuration == other.sendAnimationDuration && sendButtonColor == other.sendButtonColor && diff --git a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart index e4857a79..882d4708 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro message_list_view_theme} +@Deprecated("Use 'StreamMessageListViewTheme' instead") +typedef MessageListViewTheme = StreamMessageListViewTheme; + +/// {@template message_list_view_theme} /// Overrides the default style of [MessageListView] descendants. /// /// See also: /// -/// * [MessageListViewThemeData], which is used to configure this theme. -class MessageListViewTheme extends InheritedTheme { - /// Creates a [MessageListViewTheme]. +/// * [StreamMessageListViewThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamMessageListViewTheme extends InheritedTheme { + /// Creates a [StreamMessageListViewTheme]. /// /// The [data] parameter must not be null. - const MessageListViewTheme({ + const StreamMessageListViewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final MessageListViewThemeData data; + final StreamMessageListViewThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [MessageListViewTheme] widget, then + /// If there is no enclosing [StreamMessageListViewTheme] widget, then /// [StreamChatThemeData.messageListViewTheme] is used. /// /// Typical usage is as follows: @@ -30,35 +36,41 @@ class MessageListViewTheme extends InheritedTheme { /// ```dart /// MessageListViewTheme theme = MessageListViewTheme.of(context); /// ``` - static MessageListViewThemeData of(BuildContext context) { - final messageListViewTheme = - context.dependOnInheritedWidgetOfExactType(); + static StreamMessageListViewThemeData of(BuildContext context) { + final messageListViewTheme = context + .dependOnInheritedWidgetOfExactType(); return messageListViewTheme?.data ?? StreamChatTheme.of(context).messageListViewTheme; } @override Widget wrap(BuildContext context, Widget child) => - MessageListViewTheme(data: data, child: child); + StreamMessageListViewTheme(data: data, child: child); @override - bool updateShouldNotify(MessageListViewTheme oldWidget) => + bool updateShouldNotify(StreamMessageListViewTheme oldWidget) => data != oldWidget.data; } +/// {@macro message_list_view_theme_data} +@Deprecated("Use 'StreamMessageListViewThemeData' instead") +typedef MessageListViewThemeData = StreamMessageListViewThemeData; + +/// {@template message_list_view_theme_data} /// A style that overrides the default appearance of [MessageListView]s when -/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s +/// used with [StreamMessageListViewTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.messageListViewTheme]. /// /// See also: /// -/// * [MessageListViewTheme], the theme which is configured with this class. +/// * [StreamMessageListViewTheme], the theme which is configured with this class. /// * [StreamChatThemeData.messageListViewTheme], which can be used to override /// the default style for [MessageListView]s below the overall /// [StreamChatTheme]. -class MessageListViewThemeData with Diagnosticable { - /// Creates a [MessageListViewThemeData]. - const MessageListViewThemeData({ +/// {@endtemplate} +class StreamMessageListViewThemeData with Diagnosticable { + /// Creates a [StreamMessageListViewThemeData]. + const StreamMessageListViewThemeData({ this.backgroundColor, this.backgroundImage, }); @@ -69,12 +81,12 @@ class MessageListViewThemeData with Diagnosticable { /// The image of the [MessageListView] background. final DecorationImage? backgroundImage; - /// Copies this [MessageListViewThemeData] to another. - MessageListViewThemeData copyWith({ + /// Copies this [StreamMessageListViewThemeData] to another. + StreamMessageListViewThemeData copyWith({ Color? backgroundColor, DecorationImage? backgroundImage, }) => - MessageListViewThemeData( + StreamMessageListViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, backgroundImage: backgroundImage ?? this.backgroundImage, ); @@ -82,18 +94,18 @@ class MessageListViewThemeData with Diagnosticable { /// Linearly interpolate between two [MessageListView] themes. /// /// All the properties must be non-null. - MessageListViewThemeData lerp( - MessageListViewThemeData a, - MessageListViewThemeData b, + StreamMessageListViewThemeData lerp( + StreamMessageListViewThemeData a, + StreamMessageListViewThemeData b, double t, ) => - MessageListViewThemeData( + StreamMessageListViewThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage, ); - /// Merges one [MessageListViewThemeData] with another. - MessageListViewThemeData merge(MessageListViewThemeData? other) { + /// Merges one [StreamMessageListViewThemeData] with another. + StreamMessageListViewThemeData merge(StreamMessageListViewThemeData? other) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -104,7 +116,7 @@ class MessageListViewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is MessageListViewThemeData && + other is StreamMessageListViewThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor && backgroundImage == other.backgroundImage; diff --git a/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart index 670e05e7..83ef032a 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart @@ -2,23 +2,29 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro message_search_list_view_theme} +@Deprecated("Use 'StreamMessageSearchListViewTheme' instead") +typedef MessageSearchListViewTheme = StreamMessageSearchListViewTheme; + +/// {@template message_search_list_view_theme} /// Overrides the default style of [MessageSearchListView] descendants. /// /// See also: /// /// * [UserListViewThemeData], which is used to configure this theme. -class MessageSearchListViewTheme extends InheritedTheme { +/// {@endtemplate} +class StreamMessageSearchListViewTheme extends InheritedTheme { /// Creates a [UserListViewTheme]. /// /// The [data] parameter must not be null. - const MessageSearchListViewTheme({ + const StreamMessageSearchListViewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final MessageSearchListViewThemeData data; + final StreamMessageSearchListViewThemeData data; /// The closest instance of this class that encloses the given context. /// @@ -30,64 +36,72 @@ class MessageSearchListViewTheme extends InheritedTheme { /// ```dart /// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context); /// ``` - static MessageSearchListViewThemeData of(BuildContext context) { + static StreamMessageSearchListViewThemeData of(BuildContext context) { final messageSearchListViewTheme = context - .dependOnInheritedWidgetOfExactType(); + .dependOnInheritedWidgetOfExactType(); return messageSearchListViewTheme?.data ?? StreamChatTheme.of(context).messageSearchListViewTheme; } @override Widget wrap(BuildContext context, Widget child) => - MessageSearchListViewTheme(data: data, child: child); + StreamMessageSearchListViewTheme(data: data, child: child); @override - bool updateShouldNotify(MessageSearchListViewTheme oldWidget) => + bool updateShouldNotify(StreamMessageSearchListViewTheme oldWidget) => data != oldWidget.data; } +/// {@macro message_search_list_view_theme_data} +@Deprecated("Use 'StreamMessageSearchListViewThemeData' instead") +typedef MessageSearchListViewThemeData = StreamMessageSearchListViewThemeData; + +/// {@macro message_search_list_view_theme_data} /// A style that overrides the default appearance of [MessageSearchListView]s /// when used with [MessageSearchListView] or with the overall /// [StreamChatTheme]'s [StreamChatThemeData.messageSearchListViewTheme]. /// /// See also: /// -/// * [MessageSearchListViewTheme], the theme which is configured with this +/// * [StreamMessageSearchListViewTheme], the theme which is configured with this /// class. /// * [StreamChatThemeData.messageSearchListViewTheme], which can be used to /// override the default style for [UserListView]s below the overall /// [StreamChatTheme]. -class MessageSearchListViewThemeData with Diagnosticable { - /// Creates a [MessageSearchListViewThemeData]. - const MessageSearchListViewThemeData({ +/// {@endtemplate} +class StreamMessageSearchListViewThemeData with Diagnosticable { + /// Creates a [StreamMessageSearchListViewThemeData]. + const StreamMessageSearchListViewThemeData({ this.backgroundColor, }); /// The color of the [MessageSearchListView] background. final Color? backgroundColor; - /// Copies this [MessageSearchListViewThemeData] to another. - MessageSearchListViewThemeData copyWith({ + /// Copies this [StreamMessageSearchListViewThemeData] to another. + StreamMessageSearchListViewThemeData copyWith({ Color? backgroundColor, }) => - MessageSearchListViewThemeData( + StreamMessageSearchListViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, ); /// Linearly interpolate between two [UserListViewThemeData] themes. /// /// All the properties must be non-null. - MessageSearchListViewThemeData lerp( - MessageSearchListViewThemeData a, - MessageSearchListViewThemeData b, + StreamMessageSearchListViewThemeData lerp( + StreamMessageSearchListViewThemeData a, + StreamMessageSearchListViewThemeData b, double t, ) => - MessageSearchListViewThemeData( + StreamMessageSearchListViewThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), ); - /// Merges one [MessageSearchListViewThemeData] with another. - MessageSearchListViewThemeData merge(MessageSearchListViewThemeData? other) { + /// Merges one [StreamMessageSearchListViewThemeData] with another. + StreamMessageSearchListViewThemeData merge( + StreamMessageSearchListViewThemeData? other, + ) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -97,7 +111,7 @@ class MessageSearchListViewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is MessageSearchListViewThemeData && + other is StreamMessageSearchListViewThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor; diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart index 386a312f..c94c8159 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -2,11 +2,17 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +/// {@macro message_theme_data} +@Deprecated("Use 'StreamMessageThemeData' instead") +typedef MessageThemeData = StreamMessageThemeData; + +/// {@template message_theme_data} /// Class for getting message theme +/// {@endtemplate} // ignore: prefer-match-file-name -class MessageThemeData with Diagnosticable { - /// Creates a [MessageThemeData]. - const MessageThemeData({ +class StreamMessageThemeData with Diagnosticable { + /// Creates a [StreamMessageThemeData]. + const StreamMessageThemeData({ this.repliesStyle, this.messageTextStyle, this.messageAuthorStyle, @@ -52,13 +58,13 @@ class MessageThemeData with Diagnosticable { final Color? reactionsMaskColor; /// Theme of the avatar - final AvatarThemeData? avatarTheme; + final StreamAvatarThemeData? avatarTheme; /// Background color for messages with url attachments. final Color? linkBackgroundColor; /// Copy with a theme - MessageThemeData copyWith({ + StreamMessageThemeData copyWith({ TextStyle? messageTextStyle, TextStyle? messageAuthorStyle, TextStyle? messageLinksStyle, @@ -66,13 +72,13 @@ class MessageThemeData with Diagnosticable { TextStyle? repliesStyle, Color? messageBackgroundColor, Color? messageBorderColor, - AvatarThemeData? avatarTheme, + StreamAvatarThemeData? avatarTheme, Color? reactionsBackgroundColor, Color? reactionsBorderColor, Color? reactionsMaskColor, Color? linkBackgroundColor, }) => - MessageThemeData( + StreamMessageThemeData( messageTextStyle: messageTextStyle ?? this.messageTextStyle, messageAuthorStyle: messageAuthorStyle ?? this.messageAuthorStyle, messageLinksStyle: messageLinksStyle ?? this.messageLinksStyle, @@ -89,11 +95,15 @@ class MessageThemeData with Diagnosticable { linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor, ); - /// Linearly interpolate from one [MessageThemeData] to another. - MessageThemeData lerp(MessageThemeData a, MessageThemeData b, double t) => - MessageThemeData( - avatarTheme: - const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + /// Linearly interpolate from one [StreamMessageThemeData] to another. + StreamMessageThemeData lerp( + StreamMessageThemeData a, + StreamMessageThemeData b, + double t, + ) => + StreamMessageThemeData( + avatarTheme: const StreamAvatarThemeData() + .lerp(a.avatarTheme!, b.avatarTheme!, t), createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t), messageAuthorStyle: TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t), @@ -120,7 +130,7 @@ class MessageThemeData with Diagnosticable { ); /// Merge with a theme - MessageThemeData merge(MessageThemeData? other) { + StreamMessageThemeData merge(StreamMessageThemeData? other) { if (other == null) return this; return copyWith( messageTextStyle: messageTextStyle?.merge(other.messageTextStyle) ?? @@ -146,7 +156,7 @@ class MessageThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is MessageThemeData && + other is StreamMessageThemeData && runtimeType == other.runtimeType && messageTextStyle == other.messageTextStyle && messageAuthorStyle == other.messageAuthorStyle && diff --git a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart index ec70cf83..06ddf2d9 100644 --- a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; +/// {@macro text_theme} +@Deprecated("Use 'StreamTextTheme' instead") +typedef TextTheme = StreamTextTheme; + +/// {@template text_theme} /// Class for holding text theme -class TextTheme { +/// {@endtemplate} +class StreamTextTheme { /// Initialise light text theme - TextTheme.light({ + StreamTextTheme.light({ this.title = const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, @@ -46,7 +52,7 @@ class TextTheme { }); /// Initialise with dark theme - TextTheme.dark({ + StreamTextTheme.dark({ this.title = const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, @@ -113,7 +119,7 @@ class TextTheme { final TextStyle captionBold; /// Copy with theme - TextTheme copyWith({ + StreamTextTheme copyWith({ Brightness brightness = Brightness.light, TextStyle? body, TextStyle? title, @@ -125,7 +131,7 @@ class TextTheme { TextStyle? captionBold, }) => brightness == Brightness.light - ? TextTheme.light( + ? StreamTextTheme.light( body: body ?? this.body, title: title ?? this.title, headlineBold: headlineBold ?? this.headlineBold, @@ -135,7 +141,7 @@ class TextTheme { footnote: footnote ?? this.footnote, captionBold: captionBold ?? this.captionBold, ) - : TextTheme.dark( + : StreamTextTheme.dark( body: body ?? this.body, title: title ?? this.title, headlineBold: headlineBold ?? this.headlineBold, @@ -147,7 +153,7 @@ class TextTheme { ); /// Merge text theme - TextTheme merge(TextTheme? other) { + StreamTextTheme merge(StreamTextTheme? other) { if (other == null) return this; return copyWith( body: body.merge(other.body), diff --git a/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart index 0d99ccbd..fb57438b 100644 --- a/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart @@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +/// {@macro user_list_view_theme} +@Deprecated("Use 'StreamUserListViewTheme' instead") +typedef UserListViewTheme = StreamUserListViewTheme; + +/// {@template user_list_view_theme} /// Overrides the default style of [UserListView] descendants. /// /// See also: /// -/// * [UserListViewThemeData], which is used to configure this theme. -class UserListViewTheme extends InheritedTheme { - /// Creates a [UserListViewTheme]. +/// * [StreamUserListViewThemeData], which is used to configure this theme. +/// {@endtemplate} +class StreamUserListViewTheme extends InheritedTheme { + /// Creates a [StreamUserListViewTheme]. /// /// The [data] parameter must not be null. - const UserListViewTheme({ + const StreamUserListViewTheme({ Key? key, required this.data, required Widget child, }) : super(key: key, child: child); /// The configuration of this theme. - final UserListViewThemeData data; + final StreamUserListViewThemeData data; /// The closest instance of this class that encloses the given context. /// - /// If there is no enclosing [UserListViewTheme] widget, then + /// If there is no enclosing [StreamUserListViewTheme] widget, then /// [StreamChatThemeData.userListViewTheme] is used. /// /// Typical usage is as follows: @@ -30,35 +36,41 @@ class UserListViewTheme extends InheritedTheme { /// ```dart /// UserListViewTheme theme = UserListViewTheme.of(context); /// ``` - static UserListViewThemeData of(BuildContext context) { + static StreamUserListViewThemeData of(BuildContext context) { final userListViewTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return userListViewTheme?.data ?? StreamChatTheme.of(context).userListViewTheme; } @override Widget wrap(BuildContext context, Widget child) => - UserListViewTheme(data: data, child: child); + StreamUserListViewTheme(data: data, child: child); @override - bool updateShouldNotify(UserListViewTheme oldWidget) => + bool updateShouldNotify(StreamUserListViewTheme oldWidget) => data != oldWidget.data; } +/// {@macro user_list_view_theme_data} +@Deprecated("Use 'StreamUserListViewThemeData' instead") +typedef UserListViewThemeData = StreamUserListViewThemeData; + +/// {@template user_list_view_theme_data} /// A style that overrides the default appearance of [UserListView]s when -/// used with [UserListViewTheme] or with the overall [StreamChatTheme]'s +/// used with [StreamUserListViewTheme] or with the overall [StreamChatTheme]'s /// [StreamChatThemeData.userListViewTheme]. /// /// See also: /// -/// * [UserListViewTheme], the theme which is configured with this class. +/// * [StreamUserListViewTheme], the theme which is configured with this class. /// * [StreamChatThemeData.userListViewTheme], which can be used to override /// the default style for [UserListView]s below the overall /// [StreamChatTheme]. -class UserListViewThemeData with Diagnosticable { - /// Creates a [UserListViewThemeData]. - const UserListViewThemeData({ +/// {@endtemplate} +class StreamUserListViewThemeData with Diagnosticable { + /// Creates a [StreamUserListViewThemeData]. + const StreamUserListViewThemeData({ this.backgroundColor, }); @@ -66,27 +78,27 @@ class UserListViewThemeData with Diagnosticable { final Color? backgroundColor; /// Copies this [ChannelListViewThemeData] to another. - UserListViewThemeData copyWith({ + StreamUserListViewThemeData copyWith({ Color? backgroundColor, }) => - UserListViewThemeData( + StreamUserListViewThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, ); - /// Linearly interpolate between two [UserListViewThemeData] themes. + /// Linearly interpolate between two [StreamUserListViewThemeData] themes. /// /// All the properties must be non-null. - UserListViewThemeData lerp( - UserListViewThemeData a, - UserListViewThemeData b, + StreamUserListViewThemeData lerp( + StreamUserListViewThemeData a, + StreamUserListViewThemeData b, double t, ) => - UserListViewThemeData( + StreamUserListViewThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), ); - /// Merges one [UserListViewThemeData] with another. - UserListViewThemeData merge(UserListViewThemeData? other) { + /// Merges one [StreamUserListViewThemeData] with another. + StreamUserListViewThemeData merge(StreamUserListViewThemeData? other) { if (other == null) return this; return copyWith( backgroundColor: other.backgroundColor, @@ -96,7 +108,7 @@ class UserListViewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is UserListViewThemeData && + other is StreamUserListViewThemeData && runtimeType == other.runtimeType && backgroundColor == other.backgroundColor; diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index e7a9372f..052e1bcb 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -3,6 +3,11 @@ import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro thread_header} +@Deprecated("Use 'StreamThreadHeader' instead") +typedef ThreadHeader = StreamThreadHeader; + +/// {@template thread_header} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header_paint.png) /// @@ -56,9 +61,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The widget components render the ui based on the first ancestor of type /// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. /// Modify it to change the widget appearance. -class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { +/// {@endtemplate} +class StreamThreadHeader extends StatelessWidget + implements PreferredSizeWidget { /// Instantiate a new ThreadHeader - const ThreadHeader({ + const StreamThreadHeader({ Key? key, required this.parent, this.showBackButton = true, @@ -102,12 +109,12 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { /// if a user is typing in this thread final bool showTypingIndicator; - /// The background color of this [ThreadHeader]. + /// The background color of this [StreamThreadHeader]. final Color? backgroundColor; @override Widget build(BuildContext context) { - final channelHeaderTheme = ChannelHeaderTheme.of(context); + final channelHeaderTheme = StreamChannelHeaderTheme.of(context); final defaultSubtitle = subtitle ?? Row( @@ -162,7 +169,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { const SizedBox(height: 2), if (showTypingIndicator) Align( - child: TypingIndicator( + child: StreamTypingIndicator( channel: StreamChannel.of(context).channel, style: channelHeaderTheme.subtitleStyle, parentId: parent.id, diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index 389d1020..1cb3d8eb 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -3,10 +3,16 @@ import 'package:lottie/lottie.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +/// {@macro typing_indicator} +@Deprecated("Use 'StreamTypingIndicator' instead") +typedef TypingIndicator = StreamTypingIndicator; + +/// {@template typing_indicator} /// Widget to show the current list of typing users -class TypingIndicator extends StatelessWidget { +/// {@endtemplate} +class StreamTypingIndicator extends StatelessWidget { /// Instantiate a new TypingIndicator - const TypingIndicator({ + const StreamTypingIndicator({ Key? key, this.channel, this.alternativeWidget, diff --git a/packages/stream_chat_flutter/lib/src/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/unread_indicator.dart index 958386d1..bf23db1a 100644 --- a/packages/stream_chat_flutter/lib/src/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/unread_indicator.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro unread_indicator} +@Deprecated("Use 'StreamUnreadIndicator' instead") +typedef UnreadIndicator = StreamUnreadIndicator; + +/// {@template unread_indicator} /// Widget for showing an unread indicator -class UnreadIndicator extends StatelessWidget { - /// Constructor for creating an [UnreadIndicator] - const UnreadIndicator({ +/// {@endtemplate} +class StreamUnreadIndicator extends StatelessWidget { + /// Constructor for creating an [StreamUnreadIndicator] + const StreamUnreadIndicator({ Key? key, this.cid, }) : super(key: key); diff --git a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart index b3fe510d..d11e9609 100644 --- a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro upload_progress_indicator} +@Deprecated("Use 'StreamUploadProgressIndicator' instead") +typedef UploadProgressIndicator = StreamUploadProgressIndicator; + +/// {@template upload_progress_indicator} /// Widget for showing upload progress -class UploadProgressIndicator extends StatelessWidget { - /// Constructor for creating an [UploadProgressIndicator] - const UploadProgressIndicator({ +/// {@endtemplate} +class StreamUploadProgressIndicator extends StatelessWidget { + /// Constructor for creating an [StreamUploadProgressIndicator] + const StreamUploadProgressIndicator({ Key? key, required this.uploaded, required this.total, diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index e638685c..8d634039 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -2,10 +2,16 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro user_avatar} +@Deprecated("Use 'StreamUserAvatar' instead") +typedef UserAvatar = StreamUserAvatar; + +/// {@template user_avatar} /// Widget that displays a user avatar -class UserAvatar extends StatelessWidget { - /// Constructor to create a [UserAvatar] - const UserAvatar({ +/// {@endtemplate} +class StreamUserAvatar extends StatelessWidget { + /// Constructor to create a [StreamUserAvatar] + const StreamUserAvatar({ Key? key, required this.user, this.constraints, diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user_item.dart index c0e3482e..74ccacc2 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user_item.dart @@ -2,21 +2,26 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// +/// {@macro user_item} +@Deprecated("Use 'StreamUserItem' instead") +typedef UserItem = StreamUserItem; + +/// {@template user_item} /// It shows the current [User] preview. /// /// The widget uses a [StreamBuilder] to render the user information /// image as soon as it updates. /// /// Usually you don't use this widget as it's the default user preview used -/// by [UserListView]. +/// by [StreamUserListView]. /// /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. -class UserItem extends StatelessWidget { +/// {@endtemplate} +class StreamUserItem extends StatelessWidget { /// Instantiate a new UserItem - const UserItem({ + const StreamUserItem({ Key? key, required this.user, this.onTap, @@ -38,10 +43,10 @@ class UserItem extends StatelessWidget { /// The function called when the image is tapped final void Function(User)? onImageTap; - /// If true the [UserItem] will show a trailing checkmark + /// If true the [StreamUserItem] will show a trailing checkmark final bool selected; - /// If true the [UserItem] will show the last seen + /// If true the [StreamUserItem] will show the last seen final bool showLastOnline; @override @@ -58,7 +63,7 @@ class UserItem extends StatelessWidget { onLongPress!(user); } }, - leading: UserAvatar( + leading: StreamUserAvatar( user: user, onTap: (user) { if (onImageTap != null) { diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 58af30a7..a604c611 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -8,7 +8,11 @@ typedef UserTapCallback = void Function(User, Widget?); /// Builder used to create a custom [ListUserItem] from a [User] typedef UserItemBuilder = Widget Function(BuildContext, User, bool); -/// +/// {@macro user_list_view} +@Deprecated("Use 'StreamUserListView' instead") +typedef UserListView = StreamUserListView; + +/// {@template user_list_view} /// It shows the list of current users. /// /// ```dart @@ -42,9 +46,10 @@ typedef UserItemBuilder = Widget Function(BuildContext, User, bool); /// The widget components render the ui based on the first ancestor of /// type [StreamChatTheme]. /// Modify it to change the widget appearance. -class UserListView extends StatefulWidget { +/// {@endtemplate} +class StreamUserListView extends StatefulWidget { /// Instantiate a new UserListView - UserListView({ + StreamUserListView({ Key? key, this.filter = const Filter.empty(), this.sort, @@ -160,10 +165,10 @@ class UserListView extends StatefulWidget { final UserListController? userListController; @override - _UserListViewState createState() => _UserListViewState(); + _StreamUserListViewState createState() => _StreamUserListViewState(); } -class _UserListViewState extends State +class _StreamUserListViewState extends State with WidgetsBindingObserver { bool get _isListView => widget.crossAxisCount == 1; @@ -203,7 +208,7 @@ class _UserListViewState extends State userListController: _userListController, ); - final backgroundColor = UserListViewTheme.of(context).backgroundColor; + final backgroundColor = StreamUserListViewTheme.of(context).backgroundColor; Widget child; @@ -332,7 +337,7 @@ class _UserListViewState extends State key: ValueKey('USER-${user.id}'), child: widget.userItemBuilder != null ? widget.userItemBuilder!(context, user, selected) - : UserItem( + : StreamUserItem( user: user, onTap: (user) => widget.onUserTap!(user, widget.userWidget), onLongPress: widget.onUserLongPress, @@ -362,7 +367,7 @@ class _UserListViewState extends State : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - UserAvatar( + StreamUserAvatar( user: user, borderRadius: BorderRadius.circular(32), selected: selected, diff --git a/packages/stream_chat_flutter/lib/src/user_mention_tile.dart b/packages/stream_chat_flutter/lib/src/user_mention_tile.dart index ff9e145b..b944d912 100644 --- a/packages/stream_chat_flutter/lib/src/user_mention_tile.dart +++ b/packages/stream_chat_flutter/lib/src/user_mention_tile.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro user_mention_tile} +@Deprecated("Use 'StreamUserMentionTile' instead") +typedef UserMentionTile = StreamUserMentionTile; + +/// {@template user_mention_tile} /// This widget is used for showing user tiles for mentions /// Use [title], [subtitle], [leading], [trailing] for /// substituting widgets in respective positions -class UserMentionTile extends StatelessWidget { - /// Constructor for creating a [UserMentionTile] widget - const UserMentionTile( +/// {@endtemplate} +class StreamUserMentionTile extends StatelessWidget { + /// Constructor for creating a [StreamUserMentionTile] widget + const StreamUserMentionTile( this.user, { Key? key, this.title, @@ -41,7 +47,7 @@ class UserMentionTile extends StatelessWidget { width: 16, ), leading ?? - UserAvatar( + StreamUserAvatar( user: user, constraints: BoxConstraints.tight(const Size(40, 40)), ), diff --git a/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart b/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart index e3830231..59c86791 100644 --- a/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart @@ -6,16 +6,22 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Builder function for building a mention tile. /// -/// Use [UserMentionTile] for the default implementation. +/// Use [StreamUserMentionTile] for the default implementation. typedef MentionTileBuilder = Widget Function( BuildContext context, User user, ); +/// {@macro user_mention_tile} +@Deprecated("Use 'StreamUserMentionsOverlay' instead") +typedef UserMentionsOverlay = StreamUserMentionsOverlay; + +/// {@template user_mentions_overlay} /// Overlay for displaying users that can be mentioned. -class UserMentionsOverlay extends StatefulWidget { - /// Constructor for creating a [UserMentionsOverlay]. - UserMentionsOverlay({ +/// {@endtemplate} +class StreamUserMentionsOverlay extends StatefulWidget { + /// Constructor for creating a [StreamUserMentionsOverlay]. + StreamUserMentionsOverlay({ Key? key, required this.query, required this.channel, @@ -62,10 +68,11 @@ class UserMentionsOverlay extends StatefulWidget { final void Function(User user)? onMentionUserTap; @override - _UserMentionsOverlayState createState() => _UserMentionsOverlayState(); + _StreamUserMentionsOverlayState createState() => + _StreamUserMentionsOverlayState(); } -class _UserMentionsOverlayState extends State { +class _StreamUserMentionsOverlayState extends State { late Future> userMentionsFuture; @override @@ -75,7 +82,7 @@ class _UserMentionsOverlayState extends State { } @override - void didUpdateWidget(covariant UserMentionsOverlay oldWidget) { + void didUpdateWidget(covariant StreamUserMentionsOverlay oldWidget) { super.didUpdateWidget(oldWidget); if (widget.channel != oldWidget.channel || widget.query != oldWidget.query || @@ -116,7 +123,7 @@ class _UserMentionsOverlayState extends State { child: InkWell( onTap: () => widget.onMentionUserTap?.call(user), child: widget.mentionsTileBuilder?.call(context, user) ?? - UserMentionTile(user), + StreamUserMentionTile(user), ), ); }, diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index f820e040..3b254817 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -131,7 +131,7 @@ class StreamChannelListTile extends StatelessWidget { final channelState = channel.state!; final currentUser = channel.client.state.currentUser!; - final channelPreviewTheme = ChannelPreviewTheme.of(context); + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); final leading = this.leading ?? StreamChannelAvatar( @@ -182,7 +182,7 @@ class StreamChannelListTile extends StatelessWidget { return const Offstage(); } return unreadIndicatorBuilder?.call(context) ?? - UnreadIndicator(cid: channel.cid); + StreamUnreadIndicator(cid: channel.cid); }, ), ], @@ -213,7 +213,7 @@ class StreamChannelListTile extends StatelessWidget { padding: const EdgeInsets.only(right: 4), child: sendingIndicatorBuilder?.call(context, lastMessage) ?? - SendingIndicator( + StreamSendingIndicator( message: lastMessage, size: channelPreviewTheme.indicatorIconSize, isMessageRead: channelState @@ -318,7 +318,7 @@ class ChannelListTileSubtitle extends StatelessWidget { ], ); } - return TypingIndicator( + return StreamTypingIndicator( channel: channel, style: textStyle, alternativeWidget: ChannelLastMessageText( diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart index fd07adc8..95cc14f2 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart @@ -148,7 +148,7 @@ class StreamChannelAvatar extends StatelessWidget { return BetterStreamBuilder( stream: client.currentUserStream.map((it) => it!), initialData: currentUser, - builder: (context, user) => UserAvatar( + builder: (context, user) => StreamUserAvatar( borderRadius: borderRadius ?? previewTheme?.borderRadius, user: user, constraints: constraints ?? previewTheme?.constraints, @@ -171,7 +171,7 @@ class StreamChannelAvatar extends StatelessWidget { ), ), initialData: member, - builder: (context, member) => UserAvatar( + builder: (context, member) => StreamUserAvatar( borderRadius: borderRadius ?? previewTheme?.borderRadius, user: member.user!, constraints: constraints ?? previewTheme?.constraints, @@ -184,7 +184,7 @@ class StreamChannelAvatar extends StatelessWidget { } // Group conversation - return GroupAvatar( + return StreamGroupAvatar( channel: channel, members: otherMembers, borderRadius: borderRadius ?? previewTheme?.borderRadius, diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart index 5107131d..1a219e91 100644 --- a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart @@ -52,7 +52,7 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { Widget build(BuildContext context) { final themeData = StreamChatTheme.of(context); final colorTheme = themeData.colorTheme; - final channelPreviewTheme = ChannelPreviewTheme.of(context); + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); final currentUser = channel.client.state.currentUser; final isOneToOneChannel = channel.isDistinct && channel.memberCount == 2; @@ -84,7 +84,7 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { const SizedBox(height: 5), Center( // TODO: Refactor ChannelInfo - child: ChannelInfo( + child: StreamChannelInfo( showTypingIndicator: false, channel: channel, textStyle: channelPreviewTheme.subtitleStyle, @@ -105,7 +105,7 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { final user = member.user!; return Column( children: [ - UserAvatar( + StreamUserAvatar( user: user, constraints: const BoxConstraints( maxHeight: 64, @@ -132,7 +132,7 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { ), ), const SizedBox(height: 24), - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.user( @@ -143,7 +143,7 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { onTap: onViewInfoTap, ), if (!isOneToOneChannel) - OptionListTile( + StreamOptionListTile( title: context.translations.leaveGroupLabel, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -154,7 +154,7 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { onTap: onLeaveChannelTap, ), if (isOwner) - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.delete( @@ -165,7 +165,7 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { titleColor: colorTheme.accentError, onTap: onDeleteConversationTap, ), - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.closeSmall( diff --git a/packages/stream_chat_flutter/lib/src/video_service.dart b/packages/stream_chat_flutter/lib/src/video_service.dart index a50a43c8..fb1db9ed 100644 --- a/packages/stream_chat_flutter/lib/src/video_service.dart +++ b/packages/stream_chat_flutter/lib/src/video_service.dart @@ -66,5 +66,10 @@ class _IVideoService { } /// Get instance of [_IVideoService] +@Deprecated("Use 'StreamVideoService' instead") // ignore: non_constant_identifier_names _IVideoService get VideoService => _IVideoService.instance; + +/// Get instance of [_IVideoService] +// ignore: non_constant_identifier_names +_IVideoService get StreamVideoService => _IVideoService.instance; diff --git a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart index 2286bc3d..4d253da0 100644 --- a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart +++ b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart @@ -6,10 +6,16 @@ import 'package:stream_chat_flutter/src/video_service.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; +/// {@macro video_thumbnail_image} +@Deprecated("Use 'StreamVideoThumbnailImage' instead") +typedef VideoThumbnailImage = StreamVideoThumbnailImage; + +/// {@template video_thumbnail_image} /// Widget for creating video thumbnail image -class VideoThumbnailImage extends StatefulWidget { - /// Constructor for creating [VideoThumbnailImage] - const VideoThumbnailImage({ +/// {@endtemplate} +class StreamVideoThumbnailImage extends StatefulWidget { + /// Constructor for creating [StreamVideoThumbnailImage] + const StreamVideoThumbnailImage({ Key? key, required this.video, this.width, @@ -42,16 +48,17 @@ class VideoThumbnailImage extends StatefulWidget { final WidgetBuilder? placeholderBuilder; @override - _VideoThumbnailImageState createState() => _VideoThumbnailImageState(); + _StreamVideoThumbnailImageState createState() => + _StreamVideoThumbnailImageState(); } -class _VideoThumbnailImageState extends State { +class _StreamVideoThumbnailImageState extends State { late Future thumbnailFuture; late StreamChatThemeData _streamChatTheme; @override void initState() { - thumbnailFuture = VideoService.generateVideoThumbnail( + thumbnailFuture = StreamVideoService.generateVideoThumbnail( video: widget.video, imageFormat: widget.format, ); @@ -65,9 +72,9 @@ class _VideoThumbnailImageState extends State { } @override - void didUpdateWidget(covariant VideoThumbnailImage oldWidget) { + void didUpdateWidget(covariant StreamVideoThumbnailImage oldWidget) { if (oldWidget.video != widget.video || oldWidget.format != widget.format) { - thumbnailFuture = VideoService.generateVideoThumbnail( + thumbnailFuture = StreamVideoService.generateVideoThumbnail( video: widget.video, imageFormat: widget.format, ); diff --git a/packages/stream_chat_flutter/lib/src/visible_footnote.dart b/packages/stream_chat_flutter/lib/src/visible_footnote.dart index 700e22ad..1456de3a 100644 --- a/packages/stream_chat_flutter/lib/src/visible_footnote.dart +++ b/packages/stream_chat_flutter/lib/src/visible_footnote.dart @@ -2,10 +2,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro visible_footnote} +@Deprecated("Use 'StreamVisibleFootnote' instead") +typedef VisibleFootnote = StreamVisibleFootnote; + +/// {@template visible_footnote} /// Widget for displaying a footnote -class VisibleFootnote extends StatelessWidget { - /// Constructor for creating a [VisibleFootnote] - const VisibleFootnote({Key? key}) : super(key: key); +/// {@endtemplate} +class StreamVisibleFootnote extends StatelessWidget { + /// Constructor for creating a [StreamVisibleFootnote] + const StreamVisibleFootnote({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 83a31510..cd5c79d6 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -21,7 +21,6 @@ export 'src/gradient_avatar.dart'; export 'src/info_tile.dart'; 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'; diff --git a/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart b/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart index ed4ce267..9747f11d 100644 --- a/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart @@ -24,7 +24,7 @@ void main() { child: StreamChannel( channel: channel, child: SizedBox( - child: FileAttachment( + child: StreamFileAttachment( size: const Size( 300, 300, diff --git a/packages/stream_chat_flutter/test/src/back_button_test.dart b/packages/stream_chat_flutter/test/src/back_button_test.dart index fca30c5b..f6ec0fc4 100644 --- a/packages/stream_chat_flutter/test/src/back_button_test.dart +++ b/packages/stream_chat_flutter/test/src/back_button_test.dart @@ -136,7 +136,7 @@ void main() { ), ); - expect(find.byType(UnreadIndicator), findsOneWidget); + expect(find.byType(StreamUnreadIndicator), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index 95d2e751..6590ee66 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -58,7 +58,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader(), + body: StreamChannelHeader(), ), ), ), @@ -67,7 +67,7 @@ void main() { expect(find.text('test'), findsOneWidget); expect(find.byType(ChannelAvatar), findsOneWidget); expect(find.byType(StreamBackButton), findsOneWidget); - expect(find.byType(ChannelInfo), findsOneWidget); + expect(find.byType(StreamChannelInfo), findsOneWidget); }, ); @@ -124,7 +124,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( showConnectionStateTile: true, ), ), @@ -132,8 +132,12 @@ void main() { ), )); - expect(tester.widget(find.byType(InfoTile)).showMessage, true); - expect(tester.widget(find.byType(InfoTile)).message, + expect( + tester + .widget(find.byType(StreamInfoTile)) + .showMessage, + true); + expect(tester.widget(find.byType(StreamInfoTile)).message, 'Disconnected'); }, ); @@ -190,7 +194,7 @@ void main() { channel: channel, showLoading: false, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( showConnectionStateTile: true, ), ), @@ -200,8 +204,12 @@ void main() { await tester.pump(); - expect(tester.widget(find.byType(InfoTile)).showMessage, true); - expect(tester.widget(find.byType(InfoTile)).message, + expect( + tester + .widget(find.byType(StreamInfoTile)) + .showMessage, + true); + expect(tester.widget(find.byType(StreamInfoTile)).message, 'Reconnecting...'); }, ); @@ -257,7 +265,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( leading: Text('leading'), subtitle: Text('subtitle'), actions: [ @@ -273,7 +281,7 @@ void main() { expect(find.text('test'), findsNothing); expect(find.byType(StreamBackButton), findsNothing); expect(find.byType(ChannelAvatar), findsNothing); - expect(find.byType(ChannelInfo), findsNothing); + expect(find.byType(StreamChannelInfo), findsNothing); expect(find.text('leading'), findsOneWidget); expect(find.text('title'), findsOneWidget); expect(find.text('subtitle'), findsOneWidget); @@ -331,7 +339,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( showTypingIndicator: false, showBackButton: false, ), @@ -343,10 +351,14 @@ void main() { expect(find.byType(StreamBackButton), findsNothing); expect( tester - .widget(find.byType(ChannelInfo)) + .widget(find.byType(StreamChannelInfo)) .showTypingIndicator, false); - expect(tester.widget(find.byType(InfoTile)).showMessage, false); + expect( + tester + .widget(find.byType(StreamInfoTile)) + .showMessage, + false); }, ); @@ -405,7 +417,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( onBackPressed: () => backPressed = true, onImageTap: () => imageTapped = true, onTitleTap: () => titleTapped = true, diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index be2ade55..3fc1e718 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -169,7 +169,8 @@ void main() { ), )); - final image = tester.widget(find.byType(GroupAvatar)); + final image = + tester.widget(find.byType(StreamGroupAvatar)); final otherMembers = members.where((it) => it.userId != currentUser.id); expect( image.members.map((it) => it.user?.id), diff --git a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart index a2783067..532d07d6 100644 --- a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart @@ -22,14 +22,15 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: ChannelListHeader(), + body: StreamChannelListHeader(), ), ), ), ); await tester.pumpAndSettle(); - final userAvatar = tester.widget(find.byType(UserAvatar)); + final userAvatar = + tester.widget(find.byType(StreamUserAvatar)); expect(userAvatar.user, clientState.currentUser); expect(find.byType(StreamNeumorphicButton), findsOneWidget); expect(find.text('Stream Chat'), findsOneWidget); @@ -52,7 +53,7 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( showConnectionStateTile: true, ), ), @@ -81,7 +82,7 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( showConnectionStateTile: true, ), ), @@ -110,7 +111,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( titleBuilder: (context, status, client) => const Text('TITLE'), subtitle: const Text('SUBTITLE'), leading: const Text('LEADING'), @@ -150,7 +151,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( preNavigationCallback: () { tapped = true; }, @@ -161,7 +162,7 @@ void main() { ); await tester.pump(); - await tester.tap(find.byType(UserAvatar)); + await tester.tap(find.byType(StreamUserAvatar)); expect(tapped, true); }, ); @@ -183,7 +184,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( onUserAvatarTap: (u) { tapped++; }, @@ -197,7 +198,7 @@ void main() { ); await tester.pump(); - await tester.tap(find.byType(UserAvatar)); + await tester.tap(find.byType(StreamUserAvatar)); await tester.tap(find.byType(StreamNeumorphicButton)); expect(tapped, 2); }, diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index 47554605..63116196 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -71,7 +71,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ChannelPreview( + body: StreamChannelPreview( channel: channel, ), ), diff --git a/packages/stream_chat_flutter/test/src/date_divider_test.dart b/packages/stream_chat_flutter/test/src/date_divider_test.dart index f84d4dce..59ba4be1 100644 --- a/packages/stream_chat_flutter/test/src/date_divider_test.dart +++ b/packages/stream_chat_flutter/test/src/date_divider_test.dart @@ -19,7 +19,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: DateDivider( + body: StreamDateDivider( dateTime: DateTime.now(), ), ), diff --git a/packages/stream_chat_flutter/test/src/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/deleted_message_test.dart index 0d86e89c..171b993d 100644 --- a/packages/stream_chat_flutter/test/src/deleted_message_test.dart +++ b/packages/stream_chat_flutter/test/src/deleted_message_test.dart @@ -20,8 +20,8 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: DeletedMessage( - messageTheme: MessageThemeData( + body: StreamDeletedMessage( + messageTheme: StreamMessageThemeData( createdAtStyle: TextStyle( color: Colors.black, ), @@ -75,7 +75,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: DeletedMessage( + child: StreamDeletedMessage( messageTheme: theme.ownMessageTheme, ), ), @@ -128,7 +128,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: DeletedMessage( + child: StreamDeletedMessage( messageTheme: theme.ownMessageTheme, ), ), @@ -181,7 +181,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: DeletedMessage( + child: StreamDeletedMessage( messageTheme: theme.ownMessageTheme, reverse: true, shape: RoundedRectangleBorder( diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart index 2d271090..c17df733 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart @@ -68,7 +68,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: FullScreenMedia( + child: StreamFullScreenMedia( mediaAttachments: [ Attachment( type: 'image', diff --git a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart index 6dcc0e2d..073adc55 100644 --- a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart +++ b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart @@ -25,7 +25,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: 'demo user', userId: 'demo123'), + child: StreamGradientAvatar( + name: 'demo user', userId: 'demo123'), ), ), ), @@ -33,7 +34,7 @@ void main() { ), ); - expect(find.byType(GradientAvatar), findsOneWidget); + expect(find.byType(StreamGradientAvatar), findsOneWidget); }, ); @@ -47,7 +48,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: 'demo user', userId: 'demo123'), + child: + StreamGradientAvatar(name: 'demo user', userId: 'demo123'), ), ), ), @@ -68,7 +70,7 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: 'demo', userId: 'demo1'), + child: StreamGradientAvatar(name: 'demo', userId: 'demo1'), ), ), ), @@ -89,7 +91,7 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar( + child: StreamGradientAvatar( name: 'd123@/d de:\$as', userId: 'demo123', ), @@ -113,7 +115,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: '123@/d \$as', userId: 'demo123'), + child: StreamGradientAvatar( + name: '123@/d \$as', userId: 'demo123'), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/image_footer_test.dart b/packages/stream_chat_flutter/test/src/image_footer_test.dart index 434a78d4..7fe611b8 100644 --- a/packages/stream_chat_flutter/test/src/image_footer_test.dart +++ b/packages/stream_chat_flutter/test/src/image_footer_test.dart @@ -37,7 +37,7 @@ void main() { child: WillPopScope( onWillPop: () async => false, child: Scaffold( - body: GalleryFooter( + body: StreamGalleryFooter( message: Message(), ), ), diff --git a/packages/stream_chat_flutter/test/src/info_tile_test.dart b/packages/stream_chat_flutter/test/src/info_tile_test.dart index f175ca9a..e0687964 100644 --- a/packages/stream_chat_flutter/test/src/info_tile_test.dart +++ b/packages/stream_chat_flutter/test/src/info_tile_test.dart @@ -22,7 +22,7 @@ void main() { child: const Scaffold( body: Portal( child: SizedBox( - child: InfoTile( + child: StreamInfoTile( showMessage: true, message: 'message', child: Text('test'), @@ -52,7 +52,7 @@ void main() { child: const Scaffold( body: Portal( child: SizedBox( - child: InfoTile( + child: StreamInfoTile( showMessage: false, message: 'message', child: Text('test'), 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 69e43fba..e0f8dcb3 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 @@ -34,7 +34,7 @@ void main() { child: SizedBox( child: StreamChannel( channel: channel, - child: MessageActionsModal( + child: StreamMessageActionsModal( message: Message( text: 'test', user: User( @@ -88,7 +88,7 @@ void main() { child: SizedBox( child: StreamChannel( channel: channel, - child: MessageActionsModal( + child: StreamMessageActionsModal( showCopyMessage: false, showReplyMessage: false, showThreadReplyMessage: false, @@ -143,7 +143,7 @@ void main() { child: SizedBox( child: StreamChannel( channel: channel, - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( text: 'test', @@ -153,7 +153,7 @@ void main() { ), messageTheme: streamTheme.ownMessageTheme, customActions: [ - MessageAction( + StreamMessageAction( leading: const Icon(Icons.check), title: const Text('title'), onTap: (m) { @@ -203,7 +203,7 @@ void main() { child: SizedBox( child: StreamChannel( channel: channel, - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), onReplyTap: (m) { tapped = true; @@ -254,7 +254,7 @@ void main() { child: SizedBox( child: StreamChannel( channel: channel, - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), onThreadReplyTap: (m) { tapped = true; @@ -309,7 +309,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( text: 'test', @@ -330,7 +330,7 @@ void main() { await tester.pumpAndSettle(); - expect(find.byType(MessageInput), findsOneWidget); + expect(find.byType(StreamMessageInput), findsOneWidget); }, ); @@ -359,7 +359,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), editMessageInputBuilder: (context, m) => const Text('test'), message: Message( @@ -412,7 +412,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), onCopyTap: (m) => tapped = true, message: Message( @@ -462,7 +462,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed, @@ -512,7 +512,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed_update, @@ -560,7 +560,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -617,7 +617,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -674,7 +674,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -729,7 +729,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -786,7 +786,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', diff --git a/packages/stream_chat_flutter/test/src/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input_test.dart index 62fe0107..cdedc818 100644 --- a/packages/stream_chat_flutter/test/src/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input_test.dart @@ -59,7 +59,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: MessageInput(), + body: StreamMessageInput(), ), ), ), @@ -125,7 +125,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: MessageInput(), + body: StreamMessageInput(), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_list_view_test.dart b/packages/stream_chat_flutter/test/src/message_list_view_test.dart index b82d53b3..08e23820 100644 --- a/packages/stream_chat_flutter/test/src/message_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/message_list_view_test.dart @@ -54,7 +54,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: MessageListView( + child: StreamMessageListView( emptyBuilder: (_) => Container(key: emptyWidgetKey), ), ), @@ -63,7 +63,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.byType(MessageListView), findsOneWidget); + expect(find.byType(StreamMessageListView), findsOneWidget); expect(find.byKey(emptyWidgetKey), findsOneWidget); }); @@ -92,7 +92,7 @@ void main() { child: StreamChat( client: client, streamChatThemeData: StreamChatThemeData.light().copyWith( - messageListViewTheme: const MessageListViewThemeData( + messageListViewTheme: const StreamMessageListViewThemeData( backgroundColor: Colors.grey, backgroundImage: DecorationImage( image: AssetImage('images/placeholder.png'), @@ -102,7 +102,7 @@ void main() { ), child: StreamChannel( channel: channel, - child: const MessageListView( + child: const StreamMessageListView( key: nonEmptyWidgetKey, ), ), @@ -118,7 +118,7 @@ void main() { widget.decoration is BoxDecoration && (widget.decoration! as BoxDecoration).image != null; - expect(find.byType(MessageListView), findsOneWidget); + expect(find.byType(StreamMessageListView), findsOneWidget); expect(find.byKey(nonEmptyWidgetKey), findsOneWidget); expect( find.byWidgetPredicate( diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart index 14877ce7..31812bf2 100644 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart @@ -36,7 +36,7 @@ void main() { streamChatThemeData: streamTheme, child: StreamChannel( channel: channel, - child: MessageReactionsModal( + child: StreamMessageReactionsModal( messageWidget: const Text( 'test', key: Key('MessageWidget'), @@ -51,9 +51,9 @@ void main() { await tester.pump(const Duration(milliseconds: 1000)); - expect(find.byType(ReactionBubble), findsNothing); + expect(find.byType(StreamReactionBubble), findsNothing); - expect(find.byType(UserAvatar), findsNothing); + expect(find.byType(StreamUserAvatar), findsNothing); }, ); @@ -96,7 +96,7 @@ void main() { streamChatThemeData: streamTheme, child: StreamChannel( channel: channel, - child: MessageReactionsModal( + child: StreamMessageReactionsModal( messageWidget: const Text( 'test', key: Key('MessageWidget'), @@ -116,8 +116,8 @@ void main() { expect(find.byKey(const Key('MessageWidget')), findsOneWidget); - expect(find.byType(ReactionBubble), findsOneWidget); - expect(find.byType(UserAvatar), findsOneWidget); + expect(find.byType(StreamReactionBubble), findsOneWidget); + expect(find.byType(StreamUserAvatar), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/message_text_test.dart b/packages/stream_chat_flutter/test/src/message_text_test.dart index 57f0a282..8f2d4c10 100644 --- a/packages/stream_chat_flutter/test/src/message_text_test.dart +++ b/packages/stream_chat_flutter/test/src/message_text_test.dart @@ -65,7 +65,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: Message( text: 'demo', ), @@ -84,7 +84,7 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); - const messageTheme = MessageThemeData(); + const messageTheme = StreamMessageThemeData(); final currentUser = OwnUser( id: 'sahil', @@ -122,7 +122,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: message, messageTheme: messageTheme, ), @@ -158,7 +158,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: message, messageTheme: messageTheme, ), @@ -221,7 +221,7 @@ cool.'''; child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: Message( text: messageText, ), diff --git a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart index 6e823b43..50719b17 100644 --- a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart +++ b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart @@ -25,7 +25,7 @@ void main() { streamChatThemeData: theme, connectivityStream: Stream.value(ConnectivityResult.mobile), child: SizedBox( - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -62,7 +62,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -99,7 +99,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -144,7 +144,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -187,7 +187,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), streamChatThemeData: StreamChatThemeData.fromTheme(themeData), child: SizedBox( - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', diff --git a/packages/stream_chat_flutter/test/src/system_message_test.dart b/packages/stream_chat_flutter/test/src/system_message_test.dart index 24eccdf6..c723ff24 100644 --- a/packages/stream_chat_flutter/test/src/system_message_test.dart +++ b/packages/stream_chat_flutter/test/src/system_message_test.dart @@ -40,7 +40,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: SystemMessage( + body: StreamSystemMessage( onMessageTap: (m) => tapped = true, message: Message( text: 'demo message', @@ -51,7 +51,7 @@ void main() { ), )); - await tester.tap(find.byType(SystemMessage)); + await tester.tap(find.byType(StreamSystemMessage)); expect(find.text('demo message'), findsOneWidget); expect(tapped, true); @@ -94,7 +94,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: SystemMessage( + child: StreamSystemMessage( message: Message( text: 'demo message', ), @@ -146,7 +146,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: SystemMessage( + child: StreamSystemMessage( message: Message( text: 'demo message', ), diff --git a/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart index 63e6da86..8d055d52 100644 --- a/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart @@ -4,22 +4,23 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('AvatarThemeData copyWith, ==, hashCode basics', () { - expect(const AvatarThemeData(), const AvatarThemeData().copyWith()); - expect(const AvatarThemeData().hashCode, - const AvatarThemeData().copyWith().hashCode); + expect(const StreamAvatarThemeData(), + const StreamAvatarThemeData().copyWith()); + expect(const StreamAvatarThemeData().hashCode, + const StreamAvatarThemeData().copyWith().hashCode); }); group('AvatarThemeData lerps correctly', () { test('Lerp completely', () { expect( - const AvatarThemeData() + const StreamAvatarThemeData() .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 1), _avatarThemeDataControl2); }); test('Lerp halfway', () { expect( - const AvatarThemeData() + const StreamAvatarThemeData() .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 0.5), _avatarThemeDataControlMidLerp); }); @@ -31,9 +32,9 @@ void main() { }); } -const _avatarThemeDataControl1 = AvatarThemeData(); +const _avatarThemeDataControl1 = StreamAvatarThemeData(); -final _avatarThemeDataControlMidLerp = AvatarThemeData( +final _avatarThemeDataControlMidLerp = StreamAvatarThemeData( borderRadius: BorderRadius.circular(16), constraints: const BoxConstraints.tightFor( height: 33, @@ -41,7 +42,7 @@ final _avatarThemeDataControlMidLerp = AvatarThemeData( ), ); -final _avatarThemeDataControl2 = AvatarThemeData( +final _avatarThemeDataControl2 = StreamAvatarThemeData( borderRadius: BorderRadius.circular(12), constraints: const BoxConstraints.tightFor( height: 34, diff --git a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart index 475b1084..1b5499dd 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart @@ -4,10 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelHeaderThemeData(), - const ChannelHeaderThemeData().copyWith()); - expect(const ChannelHeaderThemeData().hashCode, - const ChannelHeaderThemeData().copyWith().hashCode); + expect(const StreamChannelHeaderThemeData(), + const StreamChannelHeaderThemeData().copyWith()); + expect(const StreamChannelHeaderThemeData().hashCode, + const StreamChannelHeaderThemeData().copyWith().hashCode); }); group('ChannelHeaderThemeData lerps', () { @@ -15,7 +15,7 @@ void main() { '''Light ChannelHeaderThemeData lerps completely to dark ChannelHeaderThemeData''', () { expect( - const ChannelHeaderThemeData() + const StreamChannelHeaderThemeData() .lerp(_channelThemeControl, _channelThemeControlDark, 1), _channelThemeControlDark); }); @@ -24,7 +24,7 @@ void main() { '''Light ChannelHeaderThemeData lerps halfway to dark ChannelHeaderThemeData''', () { expect( - const ChannelHeaderThemeData() + const StreamChannelHeaderThemeData() .lerp(_channelThemeControl, _channelThemeControlDark, 0.5), _channelThemeControlMidLerp); }); @@ -33,7 +33,7 @@ void main() { '''Dark ChannelHeaderThemeData lerps completely to light ChannelHeaderThemeData''', () { expect( - const ChannelHeaderThemeData() + const StreamChannelHeaderThemeData() .lerp(_channelThemeControlDark, _channelThemeControl, 1), _channelThemeControl); }); @@ -45,8 +45,8 @@ void main() { }); } -final _channelThemeControl = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelThemeControl = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -54,16 +54,16 @@ final _channelThemeControl = ChannelHeaderThemeData( ), ), color: const Color(0xff101418), - titleStyle: TextTheme.light().headlineBold.copyWith( + titleStyle: StreamTextTheme.light().headlineBold.copyWith( color: const Color(0xffffffff), ), - subtitleStyle: TextTheme.light().footnote.copyWith( + subtitleStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7a7a7a), ), ); -final _channelThemeControlMidLerp = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelThemeControlMidLerp = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -76,22 +76,22 @@ final _channelThemeControlMidLerp = ChannelHeaderThemeData( fontWeight: FontWeight.bold, fontSize: 16, ), - subtitleStyle: TextTheme.light().footnote.copyWith( + subtitleStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7a7a7a), ), ); -final _channelThemeControlDark = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelThemeControlDark = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - color: ColorTheme.dark().barsBg, - titleStyle: TextTheme.dark().headlineBold, - subtitleStyle: TextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().barsBg, + titleStyle: StreamTextTheme.dark().headlineBold, + subtitleStyle: StreamTextTheme.dark().footnote.copyWith( color: const Color(0xff7A7A7A), ), ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart index aa378f3a..14c09706 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart @@ -4,10 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelListHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelListHeaderThemeData(), - const ChannelListHeaderThemeData().copyWith()); - expect(const ChannelListHeaderThemeData().hashCode, - const ChannelListHeaderThemeData().copyWith().hashCode); + expect(const StreamChannelListHeaderThemeData(), + const StreamChannelListHeaderThemeData().copyWith()); + expect(const StreamChannelListHeaderThemeData().hashCode, + const StreamChannelListHeaderThemeData().copyWith().hashCode); }); group('ChannelListHeaderThemeData lerps', () { @@ -15,7 +15,7 @@ void main() { '''Light ChannelListHeaderThemeData lerps completely to dark ChannelListHeaderThemeData''', () { expect( - const ChannelListHeaderThemeData().lerp( + const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControl, _channelListHeaderThemeControlDark, 1), @@ -26,7 +26,7 @@ void main() { '''Light ChannelListHeaderThemeData lerps halfway to dark ChannelListHeaderThemeData''', () { expect( - const ChannelListHeaderThemeData().lerp( + const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControl, _channelListHeaderThemeControlDark, 0.5), @@ -37,7 +37,7 @@ void main() { '''Dark ChannelListHeaderThemeData lerps completely to light ChannelListHeaderThemeData''', () { expect( - const ChannelListHeaderThemeData().lerp( + const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControlDark, _channelListHeaderThemeControl, 1), @@ -53,20 +53,20 @@ void main() { }); } -final _channelListHeaderThemeControl = ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelListHeaderThemeControl = StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - color: ColorTheme.light().barsBg, - titleStyle: TextTheme.light().headlineBold, + color: StreamColorTheme.light().barsBg, + titleStyle: StreamTextTheme.light().headlineBold, ); -final _channelListHeaderThemeControlMidLerp = ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelListHeaderThemeControlMidLerp = StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -81,14 +81,14 @@ final _channelListHeaderThemeControlMidLerp = ChannelListHeaderThemeData( ), ); -final _channelListHeaderThemeControlDark = ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelListHeaderThemeControlDark = StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - color: ColorTheme.dark().barsBg, - titleStyle: TextTheme.dark().headlineBold, + color: StreamColorTheme.dark().barsBg, + titleStyle: StreamTextTheme.dark().headlineBold, ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart index 079c01e3..37de3140 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart @@ -6,16 +6,18 @@ import '../mocks.dart'; void main() { test('ChannelListViewThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelListViewThemeData(), - const ChannelListViewThemeData().copyWith()); + expect(const StreamChannelListViewThemeData(), + const StreamChannelListViewThemeData().copyWith()); }); test( '''Light ChannelListViewThemeData lerps completely to dark ChannelListViewThemeData''', () { expect( - const ChannelListViewThemeData().lerp(_channelListViewThemeDataControl, - _channelListViewThemeDataControlDark, 1), + const StreamChannelListViewThemeData().lerp( + _channelListViewThemeDataControl, + _channelListViewThemeDataControlDark, + 1), _channelListViewThemeDataControlDark); }); @@ -23,8 +25,10 @@ void main() { '''Light ChannelListViewThemeData lerps halfway to dark ChannelListViewThemeData''', () { expect( - const ChannelListViewThemeData().lerp(_channelListViewThemeDataControl, - _channelListViewThemeDataControlDark, 0.5), + const StreamChannelListViewThemeData().lerp( + _channelListViewThemeDataControl, + _channelListViewThemeDataControlDark, + 0.5), _channelListViewThemeDataControlHalfLerp); }); @@ -32,7 +36,7 @@ void main() { '''Dark ChannelListViewThemeData lerps completely to light ChannelListViewThemeData''', () { expect( - const ChannelListViewThemeData().lerp( + const StreamChannelListViewThemeData().lerp( _channelListViewThemeDataControlDark, _channelListViewThemeDataControl, 1), @@ -70,7 +74,7 @@ void main() { ), ); - final channelListViewTheme = ChannelListViewTheme.of(_context); + final channelListViewTheme = StreamChannelListViewTheme.of(_context); expect(channelListViewTheme.backgroundColor, _channelListViewThemeDataControl.backgroundColor); }); @@ -92,7 +96,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -100,20 +104,20 @@ void main() { ), ); - final channelListViewTheme = ChannelListViewTheme.of(_context); + final channelListViewTheme = StreamChannelListViewTheme.of(_context); expect(channelListViewTheme.backgroundColor, _channelListViewThemeDataControlDark.backgroundColor); }); } -final _channelListViewThemeDataControl = ChannelListViewThemeData( - backgroundColor: ColorTheme.light().appBg, +final _channelListViewThemeDataControl = StreamChannelListViewThemeData( + backgroundColor: StreamColorTheme.light().appBg, ); -const _channelListViewThemeDataControlHalfLerp = ChannelListViewThemeData( +const _channelListViewThemeDataControlHalfLerp = StreamChannelListViewThemeData( backgroundColor: Color(0xff818384), ); -final _channelListViewThemeDataControlDark = ChannelListViewThemeData( - backgroundColor: ColorTheme.dark().appBg, +final _channelListViewThemeDataControlDark = StreamChannelListViewThemeData( + backgroundColor: StreamColorTheme.dark().appBg, ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart index dc5c0529..782be835 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart @@ -4,10 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelPreviewThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelPreviewThemeData(), - const ChannelPreviewThemeData().copyWith()); - expect(const ChannelPreviewThemeData().hashCode, - const ChannelPreviewThemeData().copyWith().hashCode); + expect(const StreamChannelPreviewThemeData(), + const StreamChannelPreviewThemeData().copyWith()); + expect(const StreamChannelPreviewThemeData().hashCode, + const StreamChannelPreviewThemeData().copyWith().hashCode); }); group('ChannelPreviewThemeData lerps', () { @@ -15,7 +15,7 @@ void main() { '''Light ChannelPreviewThemeData lerps completely to dark ChannelPreviewThemeData''', () { expect( - const ChannelPreviewThemeData().lerp( + const StreamChannelPreviewThemeData().lerp( _channelPreviewThemeControl, _channelPreviewThemeControlDark, 1), _channelPreviewThemeControlDark); }); @@ -24,8 +24,10 @@ void main() { '''Light ChannelPreviewThemeData lerps halfway to dark ChannelPreviewThemeData''', () { expect( - const ChannelPreviewThemeData().lerp(_channelPreviewThemeControl, - _channelPreviewThemeControlDark, 0.5), + const StreamChannelPreviewThemeData().lerp( + _channelPreviewThemeControl, + _channelPreviewThemeControlDark, + 0.5), _channelPreviewThemeControlMidLerp); }); @@ -33,7 +35,7 @@ void main() { '''Dark ChannelPreviewThemeData lerps completely to light ChannelPreviewThemeData''', () { expect( - const ChannelPreviewThemeData().lerp( + const StreamChannelPreviewThemeData().lerp( _channelPreviewThemeControlDark, _channelPreviewThemeControl, 1), _channelPreviewThemeControl); }); @@ -45,28 +47,28 @@ void main() { }); } -final _channelPreviewThemeControl = ChannelPreviewThemeData( - unreadCounterColor: ColorTheme.light().accentError, - avatarTheme: AvatarThemeData( +final _channelPreviewThemeControl = StreamChannelPreviewThemeData( + unreadCounterColor: StreamColorTheme.light().accentError, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - titleStyle: TextTheme.light().bodyBold, - subtitleStyle: TextTheme.light().footnote.copyWith( + titleStyle: StreamTextTheme.light().bodyBold, + subtitleStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAtStyle: TextTheme.light().footnote.copyWith( - color: ColorTheme.light().textHighEmphasis.withOpacity(0.5), + lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( + color: StreamColorTheme.light().textHighEmphasis.withOpacity(0.5), ), indicatorIconSize: 16, ); -final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( +final _channelPreviewThemeControlMidLerp = StreamChannelPreviewThemeData( unreadCounterColor: const Color(0xffff3742), - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -82,27 +84,27 @@ final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( color: Color(0xff7a7a7a), fontSize: 12, ), - lastMessageAtStyle: TextTheme.light().footnote.copyWith( + lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0x807f7f7f).withOpacity(0.5), ), indicatorIconSize: 16, ); -final _channelPreviewThemeControlDark = ChannelPreviewThemeData( - unreadCounterColor: ColorTheme.dark().accentError, - avatarTheme: AvatarThemeData( +final _channelPreviewThemeControlDark = StreamChannelPreviewThemeData( + unreadCounterColor: StreamColorTheme.dark().accentError, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - titleStyle: TextTheme.dark().bodyBold, - subtitleStyle: TextTheme.dark().footnote.copyWith( + titleStyle: StreamTextTheme.dark().bodyBold, + subtitleStyle: StreamTextTheme.dark().footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAtStyle: TextTheme.dark().footnote.copyWith( - color: ColorTheme.dark().textHighEmphasis.withOpacity(0.5), + lastMessageAtStyle: StreamTextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().textHighEmphasis.withOpacity(0.5), ), indicatorIconSize: 16, ); diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart index 6a73e34e..0193cb35 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart @@ -7,18 +7,20 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('GalleryFooterThemeData copyWith, ==, hashCode basics', () { - expect(const GalleryFooterThemeData(), - const GalleryFooterThemeData().copyWith()); - expect(const GalleryFooterThemeData().hashCode, - const GalleryFooterThemeData().copyWith().hashCode); + expect(const StreamGalleryFooterThemeData(), + const StreamGalleryFooterThemeData().copyWith()); + expect(const StreamGalleryFooterThemeData().hashCode, + const StreamGalleryFooterThemeData().copyWith().hashCode); }); test( '''Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData''', () { expect( - const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, - _galleryFooterThemeDataControlDark, 1), + const StreamGalleryFooterThemeData().lerp( + _galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, + 1), _galleryFooterThemeDataControlDark); }); @@ -26,8 +28,10 @@ void main() { '''Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData''', () { expect( - const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, - _galleryFooterThemeDataControlDark, 0.5), + const StreamGalleryFooterThemeData().lerp( + _galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, + 0.5), _galleryFooterThemeDataControlMidLerp); }); @@ -35,8 +39,10 @@ void main() { '''Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData''', () { expect( - const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControlDark, - _galleryFooterThemeDataControl, 1), + const StreamGalleryFooterThemeData().lerp( + _galleryFooterThemeDataControlDark, + _galleryFooterThemeDataControl, + 1), _galleryFooterThemeDataControl); }); @@ -68,7 +74,7 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryFooter( + appBar: StreamGalleryFooter( message: Message(), ), ); @@ -77,7 +83,7 @@ void main() { ), ); - final imageFooterTheme = GalleryFooterTheme.of(_context); + final imageFooterTheme = StreamGalleryFooterTheme.of(_context); expect(imageFooterTheme.backgroundColor, _galleryFooterThemeDataControl.backgroundColor); expect(imageFooterTheme.shareIconColor, @@ -111,7 +117,7 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryFooter( + appBar: StreamGalleryFooter( message: Message(), ), ); @@ -120,7 +126,7 @@ void main() { ), ); - final imageFooterTheme = GalleryFooterTheme.of(_context); + final imageFooterTheme = StreamGalleryFooterTheme.of(_context); expect(imageFooterTheme.backgroundColor, _galleryFooterThemeDataControlDark.backgroundColor); expect(imageFooterTheme.shareIconColor, @@ -141,19 +147,19 @@ void main() { } // Light theme control -final _galleryFooterThemeDataControl = GalleryFooterThemeData( - backgroundColor: ColorTheme.light().barsBg, - shareIconColor: ColorTheme.light().textHighEmphasis, - titleTextStyle: TextTheme.light().headlineBold, - gridIconButtonColor: ColorTheme.light().textHighEmphasis, - bottomSheetBackgroundColor: ColorTheme.light().barsBg, - bottomSheetBarrierColor: ColorTheme.light().overlay, - bottomSheetCloseIconColor: ColorTheme.light().textHighEmphasis, - bottomSheetPhotosTextStyle: TextTheme.light().headlineBold, +final _galleryFooterThemeDataControl = StreamGalleryFooterThemeData( + backgroundColor: StreamColorTheme.light().barsBg, + shareIconColor: StreamColorTheme.light().textHighEmphasis, + titleTextStyle: StreamTextTheme.light().headlineBold, + gridIconButtonColor: StreamColorTheme.light().textHighEmphasis, + bottomSheetBackgroundColor: StreamColorTheme.light().barsBg, + bottomSheetBarrierColor: StreamColorTheme.light().overlay, + bottomSheetCloseIconColor: StreamColorTheme.light().textHighEmphasis, + bottomSheetPhotosTextStyle: StreamTextTheme.light().headlineBold, ); // Mid-lerp theme control -const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData( +const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData( backgroundColor: Color(0xff87898b), shareIconColor: Color(0xff7f7f7f), titleTextStyle: TextStyle( @@ -173,13 +179,13 @@ const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData( ); // Dark theme control -final _galleryFooterThemeDataControlDark = GalleryFooterThemeData( - backgroundColor: ColorTheme.dark().barsBg, - shareIconColor: ColorTheme.dark().textHighEmphasis, - titleTextStyle: TextTheme.dark().headlineBold, - gridIconButtonColor: ColorTheme.dark().textHighEmphasis, - bottomSheetBackgroundColor: ColorTheme.dark().barsBg, - bottomSheetBarrierColor: ColorTheme.dark().overlay, - bottomSheetCloseIconColor: ColorTheme.dark().textHighEmphasis, - bottomSheetPhotosTextStyle: TextTheme.dark().headlineBold, +final _galleryFooterThemeDataControlDark = StreamGalleryFooterThemeData( + backgroundColor: StreamColorTheme.dark().barsBg, + shareIconColor: StreamColorTheme.dark().textHighEmphasis, + titleTextStyle: StreamTextTheme.dark().headlineBold, + gridIconButtonColor: StreamColorTheme.dark().textHighEmphasis, + bottomSheetBackgroundColor: StreamColorTheme.dark().barsBg, + bottomSheetBarrierColor: StreamColorTheme.dark().overlay, + bottomSheetCloseIconColor: StreamColorTheme.dark().textHighEmphasis, + bottomSheetPhotosTextStyle: StreamTextTheme.dark().headlineBold, ); diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart index 534008ba..0ded6bbf 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart @@ -7,18 +7,20 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('GalleryHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const GalleryHeaderThemeData(), - const GalleryHeaderThemeData().copyWith()); - expect(const GalleryHeaderThemeData().hashCode, - const GalleryHeaderThemeData().copyWith().hashCode); + expect(const StreamGalleryHeaderThemeData(), + const StreamGalleryHeaderThemeData().copyWith()); + expect(const StreamGalleryHeaderThemeData().hashCode, + const StreamGalleryHeaderThemeData().copyWith().hashCode); }); test( '''Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData''', () { expect( - const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, - _galleryHeaderThemeDataDarkControl, 1), + const StreamGalleryHeaderThemeData().lerp( + _galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, + 1), _galleryHeaderThemeDataDarkControl); }); @@ -26,8 +28,10 @@ void main() { '''Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData''', () { expect( - const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, - _galleryHeaderThemeDataDarkControl, 0.5), + const StreamGalleryHeaderThemeData().lerp( + _galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, + 0.5), _galleryHeaderThemeDataHalfLerpControl); }); @@ -35,8 +39,10 @@ void main() { '''Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData''', () { expect( - const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataDarkControl, - _galleryHeaderThemeDataControl, 1), + const StreamGalleryHeaderThemeData().lerp( + _galleryHeaderThemeDataDarkControl, + _galleryHeaderThemeDataControl, + 1), _galleryHeaderThemeDataControl); }); @@ -61,7 +67,7 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryHeader( + appBar: StreamGalleryHeader( message: Message(), ), ); @@ -70,7 +76,7 @@ void main() { ), ); - final imageHeaderTheme = GalleryHeaderTheme.of(_context); + final imageHeaderTheme = StreamGalleryHeaderTheme.of(_context); expect(imageHeaderTheme.closeButtonColor, _galleryHeaderThemeDataControl.closeButtonColor); expect(imageHeaderTheme.backgroundColor, @@ -100,7 +106,7 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryHeader( + appBar: StreamGalleryHeader( message: Message(), ), ); @@ -109,7 +115,7 @@ void main() { ), ); - final imageHeaderTheme = GalleryHeaderTheme.of(_context); + final imageHeaderTheme = StreamGalleryHeaderTheme.of(_context); expect(imageHeaderTheme.closeButtonColor, _galleryHeaderThemeDataDarkControl.closeButtonColor); expect(imageHeaderTheme.backgroundColor, @@ -126,7 +132,7 @@ void main() { } // Light theme test control. -final _galleryHeaderThemeDataControl = GalleryHeaderThemeData( +final _galleryHeaderThemeDataControl = StreamGalleryHeaderThemeData( closeButtonColor: const Color(0xff000000), backgroundColor: const Color(0xffffffff), iconMenuPointColor: const Color(0xff000000), @@ -145,7 +151,7 @@ final _galleryHeaderThemeDataControl = GalleryHeaderThemeData( ); // Light theme test control. -final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData( +final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData( closeButtonColor: const Color(0xff7f7f7f), backgroundColor: const Color(0xff87898b), iconMenuPointColor: const Color(0xff7f7f7f), @@ -164,7 +170,7 @@ final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData( ); // Dark theme test control. -final _galleryHeaderThemeDataDarkControl = GalleryHeaderThemeData( +final _galleryHeaderThemeDataDarkControl = StreamGalleryHeaderThemeData( closeButtonColor: const Color(0xffffffff), backgroundColor: const Color(0xff101418), iconMenuPointColor: const Color(0xffffffff), diff --git a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart index c618d65c..3c62baea 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -4,30 +4,30 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('MessageInputThemeData copyWith, ==, hashCode basics', () { - expect(const MessageInputThemeData(), - const MessageInputThemeData().copyWith()); - expect(const MessageInputThemeData().hashCode, - const MessageInputThemeData().copyWith().hashCode); + expect(const StreamMessageInputThemeData(), + const StreamMessageInputThemeData().copyWith()); + expect(const StreamMessageInputThemeData().hashCode, + const StreamMessageInputThemeData().copyWith().hashCode); }); group('MessageInputThemeData lerps correctly', () { test('Lerp completely from light to dark', () { expect( - const MessageInputThemeData().lerp( + const StreamMessageInputThemeData().lerp( _messageInputThemeControl, _messageInputThemeControlDark, 1), _messageInputThemeControlDark); }); test('Lerp halfway from light to dark', () { expect( - const MessageInputThemeData().lerp( + const StreamMessageInputThemeData().lerp( _messageInputThemeControl, _messageInputThemeControlDark, 0.5), _messageInputThemeControlMidLerp); }); test('Lerp completely from dark to light', () { expect( - const MessageInputThemeData().lerp( + const StreamMessageInputThemeData().lerp( _messageInputThemeControlDark, _messageInputThemeControl, 1), _messageInputThemeControl); }); @@ -39,33 +39,33 @@ void main() { }); } -final _messageInputThemeControl = MessageInputThemeData( +final _messageInputThemeControl = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: ColorTheme.light().accentPrimary, - actionButtonIdleColor: ColorTheme.light().textLowEmphasis, - expandButtonColor: ColorTheme.light().accentPrimary, - sendButtonColor: ColorTheme.light().accentPrimary, - sendButtonIdleColor: ColorTheme.light().disabled, - inputBackgroundColor: ColorTheme.light().barsBg, - inputTextStyle: TextTheme.light().body, + actionButtonColor: StreamColorTheme.light().accentPrimary, + actionButtonIdleColor: StreamColorTheme.light().textLowEmphasis, + expandButtonColor: StreamColorTheme.light().accentPrimary, + sendButtonColor: StreamColorTheme.light().accentPrimary, + sendButtonIdleColor: StreamColorTheme.light().disabled, + inputBackgroundColor: StreamColorTheme.light().barsBg, + inputTextStyle: StreamTextTheme.light().body, idleBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.light().disabled, - ColorTheme.light().disabled, + StreamColorTheme.light().disabled, + StreamColorTheme.light().disabled, ], ), activeBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.light().disabled, - ColorTheme.light().disabled, + StreamColorTheme.light().disabled, + StreamColorTheme.light().disabled, ], ), ); -final _messageInputThemeControlMidLerp = MessageInputThemeData( +final _messageInputThemeControlMidLerp = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), inputBackgroundColor: const Color(0xff87898b), @@ -95,28 +95,28 @@ final _messageInputThemeControlMidLerp = MessageInputThemeData( ), ); -final _messageInputThemeControlDark = MessageInputThemeData( +final _messageInputThemeControlDark = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: ColorTheme.dark().accentPrimary, - actionButtonIdleColor: ColorTheme.dark().textLowEmphasis, - expandButtonColor: ColorTheme.dark().accentPrimary, - sendButtonColor: ColorTheme.dark().accentPrimary, - sendButtonIdleColor: ColorTheme.dark().disabled, - inputBackgroundColor: ColorTheme.dark().barsBg, - inputTextStyle: TextTheme.dark().body, + actionButtonColor: StreamColorTheme.dark().accentPrimary, + actionButtonIdleColor: StreamColorTheme.dark().textLowEmphasis, + expandButtonColor: StreamColorTheme.dark().accentPrimary, + sendButtonColor: StreamColorTheme.dark().accentPrimary, + sendButtonIdleColor: StreamColorTheme.dark().disabled, + inputBackgroundColor: StreamColorTheme.dark().barsBg, + inputTextStyle: StreamTextTheme.dark().body, idleBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.dark().disabled, - ColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, ], ), activeBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.dark().disabled, - ColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, ], ), ); diff --git a/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart index 94becc06..264710ec 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart @@ -9,18 +9,20 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('MessageListViewThemeData copyWith, ==, hashCode basics', () { - expect(const MessageListViewThemeData(), - const MessageListViewThemeData().copyWith()); - expect(const MessageListViewThemeData().hashCode, - const MessageListViewThemeData().copyWith().hashCode); + expect(const StreamMessageListViewThemeData(), + const StreamMessageListViewThemeData().copyWith()); + expect(const StreamMessageListViewThemeData().hashCode, + const StreamMessageListViewThemeData().copyWith().hashCode); }); test( '''Light MessageListViewThemeData lerps completely to dark MessageListViewThemeData''', () { expect( - const MessageListViewThemeData().lerp(_messageListViewThemeDataControl, - _messageListViewThemeDataControlDark, 1), + const StreamMessageListViewThemeData().lerp( + _messageListViewThemeDataControl, + _messageListViewThemeDataControlDark, + 1), _messageListViewThemeDataControlDark); }); @@ -28,8 +30,10 @@ void main() { '''Light MessageListViewThemeData lerps halfway to dark MessageListViewThemeData''', () { expect( - const MessageListViewThemeData().lerp(_messageListViewThemeDataControl, - _messageListViewThemeDataControlDark, 0.5), + const StreamMessageListViewThemeData().lerp( + _messageListViewThemeDataControl, + _messageListViewThemeDataControlDark, + 0.5), _messageListViewThemeDataControlHalfLerp); }); @@ -37,7 +41,7 @@ void main() { '''Dark MessageListViewThemeData lerps completely to light MessageListViewThemeData''', () { expect( - const MessageListViewThemeData().lerp( + const StreamMessageListViewThemeData().lerp( _messageListViewThemeDataControlDark, _messageListViewThemeDataControl, 1), @@ -67,7 +71,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -75,7 +79,7 @@ void main() { ), ); - final messageListViewTheme = MessageListViewTheme.of(_context); + final messageListViewTheme = StreamMessageListViewTheme.of(_context); expect(messageListViewTheme.backgroundColor, _messageListViewThemeDataControl.backgroundColor); }); @@ -97,7 +101,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -105,7 +109,7 @@ void main() { ), ); - final messageListViewTheme = MessageListViewTheme.of(_context); + final messageListViewTheme = StreamMessageListViewTheme.of(_context); expect(messageListViewTheme.backgroundColor, _messageListViewThemeDataControlDark.backgroundColor); }); @@ -128,7 +132,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -136,25 +140,25 @@ void main() { ), ); - final messageListViewTheme = MessageListViewTheme.of(_context); + final messageListViewTheme = StreamMessageListViewTheme.of(_context); expect(messageListViewTheme.backgroundImage, _messageListViewThemeDataImage.backgroundImage); }); } -final _messageListViewThemeDataControl = MessageListViewThemeData( - backgroundColor: ColorTheme.light().barsBg, +final _messageListViewThemeDataControl = StreamMessageListViewThemeData( + backgroundColor: StreamColorTheme.light().barsBg, ); -const _messageListViewThemeDataControlHalfLerp = MessageListViewThemeData( +const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData( backgroundColor: Color(0xff87898b), ); -final _messageListViewThemeDataControlDark = MessageListViewThemeData( - backgroundColor: ColorTheme.dark().barsBg, +final _messageListViewThemeDataControlDark = StreamMessageListViewThemeData( + backgroundColor: StreamColorTheme.dark().barsBg, ); -const _messageListViewThemeDataImage = MessageListViewThemeData( +const _messageListViewThemeDataImage = StreamMessageListViewThemeData( backgroundImage: DecorationImage( image: AssetImage('example/assets/background_doodle.png'), fit: BoxFit.cover, diff --git a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart index 69c63abe..bbb27463 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart @@ -6,17 +6,17 @@ import '../mocks.dart'; void main() { test('MessageSearchListViewThemeData copyWith, ==, hashCode basics', () { - expect(const MessageSearchListViewThemeData(), - const MessageSearchListViewThemeData().copyWith()); - expect(const MessageSearchListViewThemeData().hashCode, - const MessageSearchListViewThemeData().copyWith().hashCode); + expect(const StreamMessageSearchListViewThemeData(), + const StreamMessageSearchListViewThemeData().copyWith()); + expect(const StreamMessageSearchListViewThemeData().hashCode, + const StreamMessageSearchListViewThemeData().copyWith().hashCode); }); test( '''Light MessageSearchListViewThemeData lerps completely to dark MessageSearchListViewThemeData''', () { expect( - const MessageSearchListViewThemeData().lerp( + const StreamMessageSearchListViewThemeData().lerp( _messageSearchListViewThemeDataControl, _messageSearchListViewThemeDataControlDark, 1), @@ -27,7 +27,7 @@ void main() { '''Light MessageSearchListViewThemeData lerps halfway to dark MessageSearchListViewThemeData''', () { expect( - const MessageSearchListViewThemeData().lerp( + const StreamMessageSearchListViewThemeData().lerp( _messageSearchListViewThemeDataControl, _messageSearchListViewThemeDataControlDark, 0.5), @@ -38,7 +38,7 @@ void main() { '''Dark MessageSearchListViewThemeData lerps completely to light MessageSearchListViewThemeData''', () { expect( - const MessageSearchListViewThemeData().lerp( + const StreamMessageSearchListViewThemeData().lerp( _messageSearchListViewThemeDataControlDark, _messageSearchListViewThemeDataControl, 1), @@ -67,7 +67,7 @@ void main() { _context = context; return Scaffold( body: MessageSearchBloc( - child: MessageSearchListView( + child: StreamMessageSearchListView( filters: Filter.in_('members', const ['test_id']), messageQuery: 'test query', ), @@ -78,7 +78,8 @@ void main() { ), ); - final messageSearchListViewTheme = MessageSearchListViewTheme.of(_context); + final messageSearchListViewTheme = + StreamMessageSearchListViewTheme.of(_context); expect(messageSearchListViewTheme.backgroundColor, _messageSearchListViewThemeDataControl.backgroundColor); }); @@ -99,7 +100,7 @@ void main() { _context = context; return Scaffold( body: MessageSearchBloc( - child: MessageSearchListView( + child: StreamMessageSearchListView( filters: Filter.in_('members', const ['test_id']), messageQuery: 'test query', ), @@ -110,22 +111,24 @@ void main() { ), ); - final messageSearchListViewTheme = MessageSearchListViewTheme.of(_context); + final messageSearchListViewTheme = + StreamMessageSearchListViewTheme.of(_context); expect(messageSearchListViewTheme.backgroundColor, _messageSearchListViewThemeDataControlDark.backgroundColor); }); } -final _messageSearchListViewThemeDataControl = MessageSearchListViewThemeData( - backgroundColor: ColorTheme.light().appBg, +final _messageSearchListViewThemeDataControl = + StreamMessageSearchListViewThemeData( + backgroundColor: StreamColorTheme.light().appBg, ); const _messageSearchListViewThemeDataControlHalfLerp = - MessageSearchListViewThemeData( + StreamMessageSearchListViewThemeData( backgroundColor: Color(0xff818384), ); final _messageSearchListViewThemeDataControlDark = - MessageSearchListViewThemeData( - backgroundColor: ColorTheme.dark().appBg, + StreamMessageSearchListViewThemeData( + backgroundColor: StreamColorTheme.dark().appBg, ); diff --git a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart index 502be84a..3b6c3a6f 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart @@ -4,16 +4,17 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('MessageThemeData copyWith, ==, hashCode basics', () { - expect(const MessageThemeData(), const MessageThemeData().copyWith()); - expect(const MessageThemeData().hashCode, - const MessageThemeData().copyWith().hashCode); + expect(const StreamMessageThemeData(), + const StreamMessageThemeData().copyWith()); + expect(const StreamMessageThemeData().hashCode, + const StreamMessageThemeData().copyWith().hashCode); }); group('MessageThemeData lerps', () { test('''Light MessageThemeData lerps completely to dark MessageThemeData''', () { expect( - const MessageThemeData() + const StreamMessageThemeData() .lerp(_messageThemeControl, _messageThemeControlDark, 1), _messageThemeControlDark); }); @@ -21,7 +22,7 @@ void main() { test('''Dark MessageThemeData lerps completely to light MessageThemeData''', () { expect( - const MessageThemeData() + const StreamMessageThemeData() .lerp(_messageThemeControlDark, _messageThemeControl, 1), _messageThemeControl); }); @@ -33,23 +34,23 @@ void main() { }); } -final _messageThemeControl = MessageThemeData( - messageAuthorStyle: TextTheme.light().footnote.copyWith( - color: ColorTheme.light().textLowEmphasis, +final _messageThemeControl = StreamMessageThemeData( + messageAuthorStyle: StreamTextTheme.light().footnote.copyWith( + color: StreamColorTheme.light().textLowEmphasis, ), - messageTextStyle: TextTheme.light().body, - createdAtStyle: TextTheme.light().footnote.copyWith( - color: ColorTheme.light().textLowEmphasis, + messageTextStyle: StreamTextTheme.light().body, + createdAtStyle: StreamTextTheme.light().footnote.copyWith( + color: StreamColorTheme.light().textLowEmphasis, ), - repliesStyle: TextTheme.light().footnoteBold.copyWith( - color: ColorTheme.light().accentPrimary, + repliesStyle: StreamTextTheme.light().footnoteBold.copyWith( + color: StreamColorTheme.light().accentPrimary, ), - messageBackgroundColor: ColorTheme.light().disabled, - reactionsBackgroundColor: ColorTheme.light().barsBg, - reactionsBorderColor: ColorTheme.light().borders, - reactionsMaskColor: ColorTheme.light().appBg, - messageBorderColor: ColorTheme.light().disabled, - avatarTheme: AvatarThemeData( + messageBackgroundColor: StreamColorTheme.light().disabled, + reactionsBackgroundColor: StreamColorTheme.light().barsBg, + reactionsBorderColor: StreamColorTheme.light().borders, + reactionsMaskColor: StreamColorTheme.light().appBg, + messageBorderColor: StreamColorTheme.light().disabled, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -57,28 +58,28 @@ final _messageThemeControl = MessageThemeData( ), ), messageLinksStyle: TextStyle( - color: ColorTheme.light().accentPrimary, + color: StreamColorTheme.light().accentPrimary, ), - linkBackgroundColor: ColorTheme.light().linkBg, + linkBackgroundColor: StreamColorTheme.light().linkBg, ); -final _messageThemeControlDark = MessageThemeData( - messageAuthorStyle: TextTheme.dark().footnote.copyWith( - color: ColorTheme.dark().textLowEmphasis, +final _messageThemeControlDark = StreamMessageThemeData( + messageAuthorStyle: StreamTextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().textLowEmphasis, ), - messageTextStyle: TextTheme.dark().body, - createdAtStyle: TextTheme.dark().footnote.copyWith( - color: ColorTheme.dark().textLowEmphasis, + messageTextStyle: StreamTextTheme.dark().body, + createdAtStyle: StreamTextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().textLowEmphasis, ), - repliesStyle: TextTheme.dark().footnoteBold.copyWith( - color: ColorTheme.dark().accentPrimary, + repliesStyle: StreamTextTheme.dark().footnoteBold.copyWith( + color: StreamColorTheme.dark().accentPrimary, ), - messageBackgroundColor: ColorTheme.dark().disabled, - reactionsBackgroundColor: ColorTheme.dark().barsBg, - reactionsBorderColor: ColorTheme.dark().borders, - reactionsMaskColor: ColorTheme.dark().appBg, - messageBorderColor: ColorTheme.dark().disabled, - avatarTheme: AvatarThemeData( + messageBackgroundColor: StreamColorTheme.dark().disabled, + reactionsBackgroundColor: StreamColorTheme.dark().barsBg, + reactionsBorderColor: StreamColorTheme.dark().borders, + reactionsMaskColor: StreamColorTheme.dark().appBg, + messageBorderColor: StreamColorTheme.dark().disabled, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -86,7 +87,7 @@ final _messageThemeControlDark = MessageThemeData( ), ), messageLinksStyle: TextStyle( - color: ColorTheme.dark().accentPrimary, + color: StreamColorTheme.dark().accentPrimary, ), - linkBackgroundColor: ColorTheme.dark().linkBg, + linkBackgroundColor: StreamColorTheme.dark().linkBg, ); diff --git a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart index 3aa20744..6937f4b9 100644 --- a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart @@ -6,15 +6,15 @@ import '../mocks.dart'; void main() { test('UserListViewThemeData copyWith, ==, hashCode basics', () { - expect(const UserListViewThemeData(), - const UserListViewThemeData().copyWith()); + expect(const StreamUserListViewThemeData(), + const StreamUserListViewThemeData().copyWith()); }); test( '''Light UserListViewThemeData lerps completely to dark UserListViewThemeData''', () { expect( - const UserListViewThemeData().lerp(_userListViewThemeDataControl, + const StreamUserListViewThemeData().lerp(_userListViewThemeDataControl, _userListViewThemeDataControlDark, 1), _userListViewThemeDataControlDark); }); @@ -23,7 +23,7 @@ void main() { '''Light UserListViewThemeData lerps halfway to dark UserListViewThemeData''', () { expect( - const UserListViewThemeData().lerp(_userListViewThemeDataControl, + const StreamUserListViewThemeData().lerp(_userListViewThemeDataControl, _userListViewThemeDataControlDark, 0.5), _userListViewThemeDataControlHalfLerp); }); @@ -32,8 +32,10 @@ void main() { '''Dark UserListViewThemeData lerps completely to light UserListViewThemeData''', () { expect( - const UserListViewThemeData().lerp(_userListViewThemeDataControlDark, - _userListViewThemeDataControl, 1), + const StreamUserListViewThemeData().lerp( + _userListViewThemeDataControlDark, + _userListViewThemeDataControl, + 1), _userListViewThemeDataControl); }); @@ -58,7 +60,7 @@ void main() { _context = context; return Scaffold( body: UsersBloc( - child: UserListView(), + child: StreamUserListView(), ), ); }, @@ -66,7 +68,7 @@ void main() { ), ); - final userListViewTheme = UserListViewTheme.of(_context); + final userListViewTheme = StreamUserListViewTheme.of(_context); expect(userListViewTheme.backgroundColor, _userListViewThemeDataControl.backgroundColor); }); @@ -87,7 +89,7 @@ void main() { _context = context; return Scaffold( body: UsersBloc( - child: UserListView(), + child: StreamUserListView(), ), ); }, @@ -95,20 +97,20 @@ void main() { ), ); - final userListViewTheme = UserListViewTheme.of(_context); + final userListViewTheme = StreamUserListViewTheme.of(_context); expect(userListViewTheme.backgroundColor, _userListViewThemeDataControlDark.backgroundColor); }); } -final _userListViewThemeDataControl = UserListViewThemeData( - backgroundColor: ColorTheme.light().appBg, +final _userListViewThemeDataControl = StreamUserListViewThemeData( + backgroundColor: StreamColorTheme.light().appBg, ); -const _userListViewThemeDataControlHalfLerp = UserListViewThemeData( +const _userListViewThemeDataControlHalfLerp = StreamUserListViewThemeData( backgroundColor: Color(0xff818384), ); -final _userListViewThemeDataControlDark = UserListViewThemeData( - backgroundColor: ColorTheme.dark().appBg, +final _userListViewThemeDataControlDark = StreamUserListViewThemeData( + backgroundColor: StreamColorTheme.dark().appBg, ); diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/thread_header_test.dart index babe4318..6d1758fa 100644 --- a/packages/stream_chat_flutter/test/src/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/thread_header_test.dart @@ -51,7 +51,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ThreadHeader( + body: StreamThreadHeader( parent: Message(), ), ), @@ -112,7 +112,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ThreadHeader( + body: StreamThreadHeader( parent: Message(), subtitle: const Text('subtitle'), leading: const Text('leading'), diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart index 0a98aff7..27b3f977 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -71,7 +71,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: TypingIndicator( + body: StreamTypingIndicator( key: typingKey, ), ), diff --git a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart index 00359089..a470fe74 100644 --- a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart @@ -37,7 +37,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: UnreadIndicator(), + body: StreamUnreadIndicator(), ), ), ), @@ -75,7 +75,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: UnreadIndicator( + body: StreamUnreadIndicator( cid: channel.cid, ), ), @@ -115,7 +115,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: UnreadIndicator( + body: StreamUnreadIndicator( cid: channel.cid, ), ), 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 9e7b05d1..76bf3854 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -16,10 +16,7 @@ import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; /// using Flutter's [BuildContext]. /// /// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/ -@Deprecated( - "'ChannelsBloc' is deprecated and shouldn't be used. " - "Please use 'StreamChannelListController' instead.", -) +@Deprecated("Use 'StreamChannelListController' instead") class ChannelsBloc extends StatefulWidget { /// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and /// not null. From 45651576648efc08297f879027c90b42af840756 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 10 Mar 2022 14:28:33 +0530 Subject: [PATCH 109/112] chore(ui): export v3 MessageInput widget Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/message_input.dart | 1 + packages/stream_chat_flutter/lib/stream_chat_flutter.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index dc06fd91..4aa46e56 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -339,6 +339,7 @@ class MessageInput extends StatefulWidget { } /// State of [MessageInput] +@Deprecated("Use 'StreamMessageInput' instead") class MessageInputState extends State { final _attachments = {}; final List _mentionedUsers = []; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index cd5c79d6..e63db686 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -22,6 +22,7 @@ export 'src/info_tile.dart'; export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; export 'src/message_action.dart'; +export 'src/message_input.dart' show MessageInput, MessageInputState; export 'src/message_input/countdown_button.dart'; export 'src/message_input/message_input.dart'; export 'src/message_input/stream_attachment_picker.dart'; From a7b4ad3c57aa717c929a93d6dba527d889397a6f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Mar 2022 11:06:07 +0100 Subject: [PATCH 110/112] fix mention username/id replacing --- .../lib/src/channel_list_view.dart | 2 +- .../lib/src/channel_preview.dart | 19 ++++---- .../lib/src/extension.dart | 47 +++++++++++++++++++ .../lib/src/message_input/message_input.dart | 10 ++-- .../lib/src/message_text.dart | 25 +++------- .../stream_channel_list_tile.dart | 6 ++- 6 files changed, 74 insertions(+), 35 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 1113fda2..f7c39e8e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -591,7 +591,7 @@ class _ChannelListViewState extends State { decoration: BoxDecoration( color: chatThemeData.channelListViewTheme.backgroundColor, ), - child: StreamChannelPreview( + child: ChannelPreview( onLongPress: widget.onChannelLongPress, channel: channel, onImageTap: widget.onImageTap != null diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 1eb28bd2..ed5fe9f8 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -4,10 +4,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@macro channel_preview} -@Deprecated("Use 'StreamChannelPreview' instead") -typedef ChannelPreview = StreamChannelPreview; - /// {@template channel_preview} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png) @@ -24,9 +20,10 @@ typedef ChannelPreview = StreamChannelPreview; /// [StreamChatTheme]. /// Modify it to change the widget appearance. /// {@endtemplate} -class StreamChannelPreview extends StatelessWidget { - /// Constructor for creating [StreamChannelPreview] - const StreamChannelPreview({ +@Deprecated("Use 'StreamChannelListTile' instead") +class ChannelPreview extends StatelessWidget { + /// Constructor for creating [ChannelPreview] + const ChannelPreview({ required this.channel, Key? key, this.onTap, @@ -71,7 +68,7 @@ class StreamChannelPreview extends StatelessWidget { @override Widget build(BuildContext context) { - final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); final streamChatState = StreamChat.of(context); return BetterStreamBuilder( stream: channel.isMutedStream, @@ -194,13 +191,13 @@ class StreamChannelPreview extends StatelessWidget { return Text( stringDate, - style: StreamChannelPreviewTheme.of(context).lastMessageAtStyle, + style: ChannelPreviewTheme.of(context).lastMessageAtStyle, ); }, ); Widget _buildSubtitle(BuildContext context) { - final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); if (channel.isMuted) { return Row( crossAxisAlignment: CrossAxisAlignment.end, @@ -253,7 +250,7 @@ class StreamChannelPreview extends StatelessWidget { text = parts.join(' '); - final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); return Text.rich( _getDisplayText( text, diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 0feaf185..a0c9d288 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -225,3 +225,50 @@ extension UserListX on List { return entries.map((e) => e.key).toList(growable: false); } } + +/// Extensions on Message +extension MessageX on Message { + /// It replaces the user mentions with the actual user names. + Message replaceMentions({bool linkify = true}) { + var messageTextToRender = text; + for (final user in mentionedUsers.toSet()) { + final userId = user.id; + final userName = user.name; + if (linkify) { + messageTextToRender = messageTextToRender?.replaceAll( + '@$userId', + '[@$userName](@${userName.replaceAll(' ', '')})', + ); + } else { + messageTextToRender = messageTextToRender?.replaceAll( + '@$userId', + '@$userName', + ); + } + } + return copyWith(text: messageTextToRender); + } + + /// It returns the message with the translated text if available locally + Message translate(String language) => + copyWith(text: i18n?['${language}_text'] ?? text); + + /// It returns the message replacing the mentioned user names with + /// the respective user ids + Message replaceMentionsWithId() { + if (mentionedUsers.isEmpty) return this; + + var messageTextToSend = text; + if (messageTextToSend == null) return this; + + for (final user in mentionedUsers.toSet()) { + final userName = user.name; + messageTextToSend = messageTextToSend!.replaceAll( + '@$userName', + '@${user.id}', + ); + } + + return copyWith(text: messageTextToSend); + } +} 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 e7ec69f1..ed5d107c 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 @@ -1185,10 +1185,10 @@ class StreamMessageInputState extends State splits[splits.length - 1] = user.name; final rejoin = splits.join('@'); - _effectiveController.text = rejoin + - _effectiveController.text.substring( - _effectiveController.selectionStart, - ); + _effectiveController.text = + '$rejoin${_effectiveController.text.substring( + _effectiveController.selectionStart, + )}'; _onChangedDebounced.cancel(); setState(() => _showMentionsOverlay = false); @@ -1715,6 +1715,8 @@ class StreamMessageInputState extends State await streamChannel.reloadChannel(); } + message = message.replaceMentionsWithId(); + try { Future sendingFuture; if (_isEditing) { diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 012aa5c7..b6fee801 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@macro message_text} @@ -40,13 +41,14 @@ class StreamMessageText extends StatelessWidget { stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'), initialData: streamChat.currentUser!.language ?? 'en', builder: (context, language) { - final translatedText = - message.i18n?['${language}_text'] ?? message.text; - final messageText = - _replaceMentions(translatedText ?? '').replaceAll('\n', '\n\n'); + final messageText = message + .translate(language) + .replaceMentions() + .text + ?.replaceAll('\n', '\n\n'); final themeData = Theme.of(context); return MarkdownBody( - data: messageText, + data: messageText ?? '', onTapLink: ( String link, String? href, @@ -86,17 +88,4 @@ class StreamMessageText extends StatelessWidget { }, ); } - - String _replaceMentions(String text) { - var messageTextToRender = text; - for (final user in message.mentionedUsers.toSet()) { - final userId = user.id; - final userName = user.name; - messageTextToRender = messageTextToRender.replaceAll( - '@$userId', - '[@$userName](@${userName.replaceAll(' ', '')})', - ); - } - return messageTextToRender; - } } diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index 3b254817..0c78b926 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -9,6 +9,7 @@ import 'package:stream_chat_flutter/src/typing_indicator.dart'; import 'package:stream_chat_flutter/src/unread_indicator.dart'; import 'package:stream_chat_flutter/src/v4/stream_channel_avatar.dart'; import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// A widget that displays a channel preview. @@ -359,7 +360,10 @@ class ChannelLastMessageText extends StatelessWidget { if (lastMessage == null) return const Offstage(); - final lastMessageText = lastMessage.text; + final lastMessageText = lastMessage + .translate(channel.client.state.currentUser?.language ?? 'en') + .replaceMentions(linkify: false) + .text; final lastMessageAttachments = lastMessage.attachments; final lastMessageMentionedUsers = lastMessage.mentionedUsers; From d7080641bda6fab850abd932c99d0978cc437323 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 15 Mar 2022 14:24:20 +0530 Subject: [PATCH 111/112] refactor(ui): remove video compression Signed-off-by: xsahil03x --- .../lib/src/message_input.dart | 75 +++---------------- .../lib/src/message_input/message_input.dart | 44 +---------- .../stream_attachment_picker.dart | 46 +----------- .../lib/src/video_service.dart | 28 ------- packages/stream_chat_flutter/pubspec.yaml | 2 - 5 files changed, 17 insertions(+), 178 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 4aa46e56..b0cce913 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -18,12 +18,8 @@ import 'package:stream_chat_flutter/src/media_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/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:video_compress/video_compress.dart'; - -export 'package:video_compress/video_compress.dart' show VideoQuality; /// A callback that can be passed to [MessageInput.onError]. /// @@ -191,8 +187,6 @@ class MessageInput extends StatefulWidget { this.mentionsTileBuilder, this.userMentionsTileBuilder, this.maxAttachmentSize = _kDefaultMaxAttachmentSize, - this.compressedVideoQuality = VideoQuality.DefaultQuality, - this.compressedVideoFrameRate = 30, this.onError, this.attachmentLimit = 10, this.onAttachmentLimitExceed, @@ -213,12 +207,6 @@ class MessageInput extends StatefulWidget { /// Message to edit final Message? editMessage; - /// 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 @@ -1119,41 +1107,18 @@ class MessageInputState extends State { if (mediaFile == null) return; - var file = AttachmentFile( + final 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 StreamVideoService.compressVideo( - file.path!, - frameRate: widget.compressedVideoFrameRate, - quality: widget.compressedVideoQuality, - ); - - if (mediaInfo == null || - 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( + return _showErrorAlert( + context.translations.fileTooLargeError( widget.maxAttachmentSize / (1024 * 1024), - )); - return; - } + ), + ); } setState(() { @@ -1678,33 +1643,11 @@ class MessageInputState extends State { ); if (file.size! > widget.maxAttachmentSize) { - if (attachmentType == 'video' && file.path != null) { - final mediaInfo = await (StreamVideoService.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( + return _showErrorAlert( + context.translations.fileTooLargeError( widget.maxAttachmentSize / (1024 * 1024), - )); - return; - } + ), + ); } setState(() { 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 e7ec69f1..21d5775d 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 @@ -18,12 +18,8 @@ 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'; -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: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); @@ -202,8 +198,6 @@ class StreamMessageInput extends StatefulWidget { this.mentionsTileBuilder, this.userMentionsTileBuilder, this.maxAttachmentSize = _kDefaultMaxAttachmentSize, - this.compressedVideoQuality = VideoQuality.DefaultQuality, - this.compressedVideoFrameRate = 30, this.onError, this.attachmentLimit = 10, this.onAttachmentLimitExceed, @@ -224,12 +218,6 @@ class StreamMessageInput extends StatefulWidget { /// List of options for showing overlays. final List customOverlays; - /// 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 @@ -1126,8 +1114,6 @@ class StreamMessageInputState extends State attachmentLimit: widget.attachmentLimit, onAttachmentLimitExceeded: widget.onAttachmentLimitExceed, maxAttachmentSize: widget.maxAttachmentSize, - compressedVideoQuality: widget.compressedVideoQuality, - compressedVideoFrameRate: widget.compressedVideoFrameRate, onError: _showErrorAlert, ); @@ -1639,33 +1625,11 @@ class StreamMessageInputState extends State ); if (file.size! > widget.maxAttachmentSize) { - if (attachmentType == 'video' && file.path != null) { - final mediaInfo = await (StreamVideoService.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( + return _showErrorAlert( + context.translations.fileTooLargeError( widget.maxAttachmentSize / (1024 * 1024), - )); - return; - } + ), + ); } _addAttachments([ 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 1e1b99c7..2c254223 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 @@ -5,9 +5,7 @@ import 'package:flutter_svg/flutter_svg.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'; /// Callback for when a file has to be picked. typedef FilePickerCallback = void Function( @@ -34,8 +32,6 @@ class StreamAttachmentPicker extends StatefulWidget { this.attachmentLimit = 10, this.onAttachmentLimitExceeded, this.maxAttachmentSize = 20971520, - this.compressedVideoQuality = VideoQuality.DefaultQuality, - this.compressedVideoFrameRate = 30, this.onError, this.allowedAttachmentTypes = const [ DefaultAttachmentTypes.image, @@ -66,12 +62,6 @@ class StreamAttachmentPicker extends StatefulWidget { /// 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 @@ -94,8 +84,6 @@ class StreamAttachmentPicker extends StatefulWidget { int? attachmentLimit, AttachmentLimitExceedListener? onAttachmentLimitExceeded, int? maxAttachmentSize, - VideoQuality? compressedVideoQuality, - int? compressedVideoFrameRate, ValueChanged? onChangeInputState, ValueChanged? onError, List? allowedAttachmentTypes, @@ -112,10 +100,6 @@ class StreamAttachmentPicker extends StatefulWidget { onAttachmentLimitExceeded: onAttachmentLimitExceeded ?? this.onAttachmentLimitExceeded, maxAttachmentSize: maxAttachmentSize ?? this.maxAttachmentSize, - compressedVideoQuality: - compressedVideoQuality ?? this.compressedVideoQuality, - compressedVideoFrameRate: - compressedVideoFrameRate ?? this.compressedVideoFrameRate, onError: onError ?? this.onError, allowedAttachmentTypes: allowedAttachmentTypes ?? this.allowedAttachmentTypes, @@ -375,33 +359,11 @@ class _StreamAttachmentPickerState extends State { ); if (file.size! > widget.maxAttachmentSize) { - if (medium.type == AssetType.video && file.path != null) { - final mediaInfo = await (StreamVideoService.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( + return widget.onError?.call( + context.translations.fileTooLargeError( widget.maxAttachmentSize / (1024 * 1024), - )); - return; - } + ), + ); } setState(() { diff --git a/packages/stream_chat_flutter/lib/src/video_service.dart b/packages/stream_chat_flutter/lib/src/video_service.dart index fb1db9ed..120460be 100644 --- a/packages/stream_chat_flutter/lib/src/video_service.dart +++ b/packages/stream_chat_flutter/lib/src/video_service.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:synchronized/synchronized.dart'; -import 'package:video_compress/video_compress.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; /// @@ -12,32 +10,6 @@ class _IVideoService { /// Singleton instance of [_IVideoService] static final _IVideoService instance = _IVideoService._(); - final _lock = Lock(); - - /// compress video from [path] - /// compress video from [path] return [Future] - /// - /// you can choose its [quality] and [frameRate] - /// - /// ## example - /// ```dart - /// final info = await _flutterVideoCompress.compressVideo( - /// file.path, - /// ); - /// debugPrint(info.toJson()); - /// ``` - Future compressVideo( - String path, { - int frameRate = 30, - VideoQuality quality = VideoQuality.DefaultQuality, - }) async => - _lock.synchronized( - () => VideoCompress.compressVideo( - path, - frameRate: frameRate, - quality: quality, - ), - ); /// Generates a thumbnail image data in memory as UInt8List, /// it can be easily used by Image.memory(...). diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index c2d90eb0..5dab31a1 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -38,9 +38,7 @@ dependencies: shimmer: ^2.0.0 stream_chat_flutter_core: ^3.5.0 substring_highlight: ^1.0.26 - synchronized: ^3.0.0 url_launcher: ^6.0.3 - video_compress: ^3.0.0 video_player: ^2.1.0 video_thumbnail: ^0.4.3 visibility_detector: ^0.2.0 From 2b09747cb1e5a1a638acd118fd94a05cb5a6e54d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Mar 2022 14:02:51 +0530 Subject: [PATCH 112/112] fix(ui): fix channel list tile style for mentioned users Signed-off-by: xsahil03x --- .../stream_channel_list_tile.dart | 36 ++++++++++++++++--- .../test/src/channel_preview_test.dart | 2 +- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart index 0c78b926..16fcc5cc 100644 --- a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -367,6 +367,11 @@ class ChannelLastMessageText extends StatelessWidget { final lastMessageAttachments = lastMessage.attachments; final lastMessageMentionedUsers = lastMessage.mentionedUsers; + final mentionedUsersRegex = RegExp( + lastMessageMentionedUsers.map((it) => '@${it.name}').join('|'), + caseSensitive: false, + ); + final messageTextParts = [ ...lastMessageAttachments.map((it) { if (it.type == 'image') { @@ -380,7 +385,11 @@ class ChannelLastMessageText extends StatelessWidget { ? (it.title ?? 'File') : '${it.title ?? 'File'} , '; }), - if (lastMessageText != null) lastMessageText, + if (lastMessageText != null) + if (lastMessageMentionedUsers.isNotEmpty) + ...mentionedUsersRegex.allMatchesWithSep(lastMessageText) + else + lastMessageText, ]; final fontStyle = (lastMessage.isSystem || lastMessage.isDeleted) @@ -399,7 +408,7 @@ class ChannelLastMessageText extends StatelessWidget { if (lastMessageMentionedUsers.isNotEmpty && lastMessageMentionedUsers.any((it) => '@${it.name}' == part)) TextSpan( - text: '$part ', + text: part, style: mentionsTextStyle, ) else if (lastMessageAttachments.isNotEmpty && @@ -407,12 +416,14 @@ class ChannelLastMessageText extends StatelessWidget { .where((it) => it.title != null) .any((it) => it.title == part)) TextSpan( - text: '$part ', - style: regularTextStyle, + text: part, + style: regularTextStyle?.copyWith( + fontStyle: FontStyle.italic, + ), ) else TextSpan( - text: part == messageTextParts.last ? part : '$part ', + text: part, style: regularTextStyle, ), ]; @@ -426,3 +437,18 @@ class ChannelLastMessageText extends StatelessWidget { }, ); } + +extension _RegExpX on RegExp { + List allMatchesWithSep(String input, [int start = 0]) { + final result = []; + for (final match in allMatches(input, start)) { + result.add(input.substring(start, match.start)); + // ignore: cascade_invocations + result.add(match[0]!); + // ignore: parameter_assignments + start = match.end; + } + result.add(input.substring(start)); + return result; + } +} diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index 63116196..47554605 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -71,7 +71,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: StreamChannelPreview( + body: ChannelPreview( channel: channel, ), ),