From 9dba5a61d2a304d7702da1a9a0883ce23226cd98 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 17:01:50 +0530 Subject: [PATCH 01/12] feat(ui): add possibility to limit attachments in `MessageInput` Signed-off-by: xsahil03x --- .../stream_chat_flutter/example/lib/main.dart | 2 +- .../lib/src/message_input.dart | 462 +++++++++--------- .../stream_chat_flutter/lib/src/utils.dart | 48 ++ 3 files changed, 268 insertions(+), 244 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index eb03f89f..3d292b09 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -106,7 +106,7 @@ class ChannelPage extends StatelessWidget { Expanded( child: MessageListView(), ), - MessageInput(), + MessageInput(attachmentLimit: 3), ], ), ); diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 9431cd22..4f61258c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -37,6 +37,16 @@ typedef ErrorListener = void Function( 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, @@ -164,7 +174,13 @@ class MessageInput extends StatefulWidget { this.compressedVideoQuality = VideoQuality.DefaultQuality, this.compressedVideoFrameRate = 30, this.onError, - }) : super(key: key); + this.attachmentLimit = 10, + this.onAttachmentLimitExceed, + }) : assert( + initialMessage == null || editMessage == null, + "Can't provide both `initialMessage` and `editMessage`", + ), + super(key: key); /// Message to edit final Message? editMessage; @@ -247,6 +263,11 @@ class MessageInput extends StatefulWidget { /// A callback for error reporting final ErrorListener? onError; + final int attachmentLimit; + + /// A callback for error reporting + final AttachmentLimitExceedListener? onAttachmentLimitExceed; + @override MessageInputState createState() => MessageInputState(); @@ -270,7 +291,6 @@ class MessageInputState extends State { final _imagePicker = ImagePicker(); late final FocusNode _focusNode; bool _inputEnabled = true; - bool _messageIsPresent = false; bool _commandEnabled = false; OverlayEntry? _commandsOverlay, _mentionsOverlay, _emojiOverlay; late Iterable _emojiNames; @@ -285,13 +305,15 @@ class MessageInputState extends State { KeyboardVisibilityController(); /// The editing controller passed to the input TextField - late final TextEditingController textEditingController; + late final TextEditingController _textEditingController; late StreamChatThemeData _streamChatTheme; late MessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => widget.quotedMessage != null; + bool get _messageIsPresent => _textEditingController.text.trim().isNotEmpty; + @override void initState() { super.initState(); @@ -303,19 +325,19 @@ class MessageInputState extends State { _keyboardListener = _keyboardVisibilityController.onChange.listen((visible) { if (_focusNode.hasFocus) { - _onChanged(context, textEditingController.text); + _onChanged(context, _textEditingController.text); } }); } - textEditingController = + _textEditingController = widget.textEditingController ?? TextEditingController(); if (widget.editMessage != null || widget.initialMessage != null) { _parseExistingMessage(widget.editMessage ?? widget.initialMessage!); } - textEditingController.addListener(() { - _onChanged(context, textEditingController.text); + _textEditingController.addListener(() { + _onChanged(context, _textEditingController.text); }); _focusNode.addListener(() { @@ -582,7 +604,7 @@ class MessageInputState extends State { maxLines: null, onSubmitted: (_) => sendMessage(), keyboardType: widget.keyboardType, - controller: textEditingController, + controller: _textEditingController, focusNode: _focusNode, style: _messageInputTheme.inputTextStyle, autofocus: widget.autofocus, @@ -728,7 +750,6 @@ class MessageInputState extends State { .catchError((e) {}); setState(() { - _messageIsPresent = s.trim().isNotEmpty; _actionsShrunk = s.trim().isNotEmpty && ((widget.actions?.length ?? 0) + (widget.showCommandsButton ? 1 : 0) + @@ -764,15 +785,15 @@ class MessageInputState extends State { void _checkEmoji(String s, BuildContext context) { if (s.isNotEmpty && - textEditingController.selection.baseOffset > 0 && - textEditingController.text + _textEditingController.selection.baseOffset > 0 && + _textEditingController.text .substring( 0, - textEditingController.selection.baseOffset, + _textEditingController.selection.baseOffset, ) .contains(':')) { - final textToSelection = textEditingController.text - .substring(0, textEditingController.value.selection.start); + 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); @@ -791,9 +812,9 @@ class MessageInputState extends State { void _checkMentions(String s, BuildContext context) { if (s.isNotEmpty && - textEditingController.selection.baseOffset > 0 && - textEditingController.text - .substring(0, textEditingController.selection.baseOffset) + _textEditingController.selection.baseOffset > 0 && + _textEditingController.text + .substring(0, _textEditingController.selection.baseOffset) .split(' ') .last .contains('@')) { @@ -816,8 +837,7 @@ class MessageInputState extends State { if (matchedCommandsList.length == 1) { _chosenCommand = matchedCommandsList[0]; - textEditingController.clear(); - _messageIsPresent = false; + _textEditingController.clear(); setState(() { _commandEnabled = true; }); @@ -833,7 +853,7 @@ class MessageInputState extends State { } OverlayEntry? _buildCommandsOverlayEntry() { - final text = textEditingController.text.trimLeft(); + final text = _textEditingController.text.trimLeft(); final commands = StreamChannel.of(context) .channel .config @@ -1037,7 +1057,7 @@ class MessageInputState extends State { onPressed: _attachmentContainsFile && _attachments.isNotEmpty ? null : () { - pickFile(DefaultAttachmentTypes.image, true); + pickFile(DefaultAttachmentTypes.image, camera: true); }, ), IconButton( @@ -1048,7 +1068,7 @@ class MessageInputState extends State { onPressed: _attachmentContainsFile && _attachments.isNotEmpty ? null : () { - pickFile(DefaultAttachmentTypes.video, true); + pickFile(DefaultAttachmentTypes.video, camera: true); }, ), ], @@ -1107,7 +1127,7 @@ class MessageInputState extends State { if (_attachments.containsKey(media.id)) { setState(() => _attachments.remove(media.id)); } else { - _addAttachment(media); + _addAssetAttachment(media); } }, ), @@ -1119,15 +1139,13 @@ class MessageInputState extends State { ); } - void _addAttachment(AssetEntity medium) async { + void _addAssetAttachment(AssetEntity medium) async { final mediaFile = await medium.originFile.timeout( const Duration(seconds: 5), onTimeout: () => medium.originFile, ); - if (mediaFile == null) { - return; - } + if (mediaFile == null) return; var file = AttachmentFile( path: mediaFile.path, @@ -1166,11 +1184,12 @@ class MessageInputState extends State { } setState(() { - _attachments[medium.id] = Attachment( + final attachment = Attachment( id: medium.id, file: file, type: medium.type == AssetType.image ? 'image' : 'video', ); + _addAttachments([attachment]); }); } @@ -1251,8 +1270,8 @@ class MessageInputState extends State { } OverlayEntry? _buildMentionsOverlayEntry() { - final splits = textEditingController.text - .substring(0, textEditingController.value.selection.start) + final splits = _textEditingController.text + .substring(0, _textEditingController.value.selection.start) .split('@'); final query = splits.last.toLowerCase(); @@ -1314,10 +1333,10 @@ class MessageInputState extends State { splits[splits.length - 1] = m.user!.name; final rejoin = splits.join('@'); - textEditingController.value = TextEditingValue( + _textEditingController.value = TextEditingValue( text: rejoin + - textEditingController.text.substring( - textEditingController.selection.start), + _textEditingController.text.substring( + _textEditingController.selection.start), selection: TextSelection.collapsed( offset: rejoin.length, ), @@ -1361,8 +1380,8 @@ class MessageInputState extends State { } OverlayEntry? _buildEmojiOverlay() { - final splits = textEditingController.text - .substring(0, textEditingController.value.selection.start) + final splits = _textEditingController.text + .substring(0, _textEditingController.value.selection.start) .split(':'); final query = splits.last.toLowerCase(); @@ -1473,10 +1492,10 @@ class MessageInputState extends State { void _chooseEmoji(List splits, Emoji emoji) { final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; - textEditingController.value = TextEditingValue( + _textEditingController.value = TextEditingValue( text: rejoin + - textEditingController.text - .substring(textEditingController.selection.start), + _textEditingController.text + .substring(_textEditingController.selection.start), selection: TextSelection.collapsed( offset: rejoin.length, ), @@ -1487,11 +1506,10 @@ class MessageInputState extends State { } void _setCommand(Command c) { - textEditingController.clear(); + _textEditingController.clear(); setState(() { _chosenCommand = c; _commandEnabled = true; - _messageIsPresent = false; }); _commandsOverlay?.remove(); _commandsOverlay = null; @@ -1682,7 +1700,7 @@ class MessageInputState extends State { } Widget _buildCommandButton() { - final s = textEditingController.text.trim(); + final s = _textEditingController.text.trim(); return IconButton( icon: StreamSvgIcon.lightning( @@ -1768,87 +1786,100 @@ class MessageInputState extends State { }); } else { showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), + 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); - }, - ), - if (!kIsWeb) - ListTile( - leading: const Icon(Icons.camera_alt), - title: Text(context.translations.photoFromCameraLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.image, true); - Navigator.pop(context); - }, - ), - if (!kIsWeb) - ListTile( - leading: const Icon(Icons.videocam), - title: Text(context.translations.videoFromCameraLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.video, true); - Navigator.pop(context); - }, - ), - ListTile( - leading: const Icon(Icons.insert_drive_file), - title: Text(context.translations.uploadAFileLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.file); - Navigator.pop(context); - }, - ), - ], - )); + ), + 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); + }, + ), + if (!kIsWeb) + ListTile( + leading: const Icon(Icons.camera_alt), + title: Text(context.translations.photoFromCameraLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.image, camera: true); + Navigator.pop(context); + }, + ), + if (!kIsWeb) + ListTile( + leading: const Icon(Icons.videocam), + title: Text(context.translations.videoFromCameraLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.video, camera: true); + 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 - void addAttachment(Attachment attachment) { - setState(() { - _attachments[attachment.id] = attachment.copyWith( - uploadState: attachment.uploadState, + /// Adds an attachment to the [_attachments] map + void _addAttachments(Iterable attachments) { + final length = _attachments.length + attachments.length; + if (length > widget.attachmentLimit) { + final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; + if (onAttachmentLimitExceed != null) { + return onAttachmentLimitExceed( + widget.attachmentLimit, + 'Attachment Limit crossed ${widget.attachmentLimit}', + ); + } + return _showErrorAlert( + 'Attachment Limit crossed ${widget.attachmentLimit}', ); - }); + } + for (final attachment in attachments) { + _attachments[attachment.id] = attachment; + } } /// Pick a file from the device /// If [camera] is true then the camera will open - // ignore: avoid_positional_boolean_parameters - void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async { + void pickFile( + DefaultAttachmentTypes fileType, { + bool camera = false, + }) async { setState(() => _inputEnabled = false); AttachmentFile? file; @@ -1947,16 +1978,14 @@ class MessageInputState extends State { } } - _attachments[attachment.id] = attachment; - setState(() { - _attachments.update( - attachment.id, - (it) => it.copyWith( - file: file, - extraData: {...it.extraData} - ..update('file_size', ((_) => file!.size!)), - )); + _addAttachments([ + attachment.copyWith( + file: file, + extraData: {...attachment.extraData} + ..update('file_size', ((_) => file!.size!)), + ), + ]); }); } @@ -2005,7 +2034,7 @@ class MessageInputState extends State { /// Sends the current message Future sendMessage() async { - var text = textEditingController.text.trim(); + var text = _textEditingController.text.trim(); if (text.isEmpty && _attachments.isEmpty) { return; } @@ -2018,12 +2047,11 @@ class MessageInputState extends State { final attachments = [..._attachments.values]; - textEditingController.clear(); + _textEditingController.clear(); _attachments.clear(); widget.onQuotedMessageCleared?.call(); setState(() { - _messageIsPresent = false; _commandEnabled = false; }); @@ -2153,7 +2181,8 @@ class MessageInputState extends State { child: Text( context.translations.okLabel, style: _streamChatTheme.textTheme.bodyBold.copyWith( - color: _streamChatTheme.colorTheme.accentPrimary), + color: _streamChatTheme.colorTheme.accentPrimary, + ), ), ), ], @@ -2164,13 +2193,8 @@ class MessageInputState extends State { } void _parseExistingMessage(Message message) { - textEditingController.text = message.text!; - _messageIsPresent = true; - for (final attachment in message.attachments) { - _attachments[attachment.id] = attachment.copyWith( - uploadState: attachment.uploadState, - ); - } + _textEditingController.text = message.text!; + _addAttachments(message.attachments); } @override @@ -2196,54 +2220,6 @@ class MessageInputState extends State { } } -/// Represents a 2-tuple, or pair. -class Tuple2 { - /// Creates a new tuple value with the specified items. - const Tuple2(this.item1, this.item2); - - /// Create a new tuple value with the specified list [items]. - factory Tuple2.fromList(List items) { - if (items.length != 2) { - throw ArgumentError('items must have length 2'); - } - - return Tuple2(items[0] as T1, items[1] as T2); - } - - /// Returns the first item of the tuple - final T1 item1; - - /// Returns the second item of the tuple - final T2 item2; - - /// Returns a tuple with the first item set to the specified value. - Tuple2 withItem1(T1 v) => Tuple2(v, item2); - - /// Returns a tuple with the second item set to the specified value. - Tuple2 withItem2(T2 v) => Tuple2(item1, v); - - /// Creates a [List] containing the items of this [Tuple2]. - /// - /// The elements are in item order. The list is variable-length - /// if [growable] is true. - List toList({bool growable = false}) => - List.from([item1, item2], growable: growable); - - @override - String toString() => '[$item1, $item2]'; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Tuple2 && - runtimeType == other.runtimeType && - item1 == other.item1 && - item2 == other.item2; - - @override - int get hashCode => item1.hashCode ^ item2.hashCode; -} - class _PickerWidget extends StatefulWidget { const _PickerWidget({ Key? key, @@ -2281,74 +2257,74 @@ class _PickerWidgetState extends State<_PickerWidget> { return const Offstage(); } return FutureBuilder( - future: requestPermission, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } + future: requestPermission, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } - 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, + 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.addMoreFilesLabel, - style: TextStyle( + context.translations.allowGalleryAccessMessage, + style: widget.streamChatTheme.textTheme.bodyBold.copyWith( 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/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 314a788a..d97ea6a1 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -340,3 +340,51 @@ Widget wrapAttachmentWidget( type: MaterialType.transparency, child: attachmentWidget, ); + +/// Represents a 2-tuple, or pair. +class Tuple2 { + /// Creates a new tuple value with the specified items. + const Tuple2(this.item1, this.item2); + + /// Create a new tuple value with the specified list [items]. + factory Tuple2.fromList(List items) { + if (items.length != 2) { + throw ArgumentError('items must have length 2'); + } + + return Tuple2(items[0] as T1, items[1] as T2); + } + + /// Returns the first item of the tuple + final T1 item1; + + /// Returns the second item of the tuple + final T2 item2; + + /// Returns a tuple with the first item set to the specified value. + Tuple2 withItem1(T1 v) => Tuple2(v, item2); + + /// Returns a tuple with the second item set to the specified value. + Tuple2 withItem2(T2 v) => Tuple2(item1, v); + + /// Creates a [List] containing the items of this [Tuple2]. + /// + /// The elements are in item order. The list is variable-length + /// if [growable] is true. + List toList({bool growable = false}) => + List.from([item1, item2], growable: growable); + + @override + String toString() => '[$item1, $item2]'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Tuple2 && + runtimeType == other.runtimeType && + item1 == other.item1 && + item2 == other.item2; + + @override + int get hashCode => item1.hashCode ^ item2.hashCode; +} From 0b33c258f599e2b4c49192022edc277c4d092c5d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 18:14:43 +0530 Subject: [PATCH 02/12] feat(ui, localization): Move attachment limit exceeded error to translations Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 3 +++ .../lib/src/localization/translations.dart | 6 ++++++ .../stream_chat_flutter/lib/src/message_input.dart | 12 ++++++++---- .../example/lib/add_new_lang.dart | 4 ++++ .../lib/src/stream_chat_localizations_en.dart | 4 ++++ .../lib/src/stream_chat_localizations_es.dart | 6 ++++++ .../lib/src/stream_chat_localizations_fr.dart | 6 ++++++ .../lib/src/stream_chat_localizations_hi.dart | 6 ++++++ .../lib/src/stream_chat_localizations_it.dart | 6 ++++++ .../test/translations_test.dart | 1 + 10 files changed, 50 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 5289ec30..464e40ea 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -10,6 +10,9 @@ - `GalleryHeader` - `GalleryFooter` - `ThreadHeader` +- Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message. +- Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded. + This will override the default error alert behaviour. 🔄 Changed diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index d6e5add0..e5dd5df3 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -304,6 +304,8 @@ abstract class Translations { /// The label for "Reply to message" String get replyToMessageLabel; + + String attachmentLimitExceedError(int limit); } /// Default implementation of Translation strings for the stream chat widgets @@ -664,4 +666,8 @@ class DefaultTranslations implements Translations { @override String get replyToMessageLabel => 'Reply to Message'; + + @override + String attachmentLimitExceedError(int limit) => + 'Attachment limit exceeded, limit: $limit'; } diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 4f61258c..bce164f0 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -263,9 +263,12 @@ class MessageInput extends StatefulWidget { /// 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 error reporting + /// A callback for when the [attachmentLimit] is exceeded. + /// + /// This will override the default error alert behaviour. final AttachmentLimitExceedListener? onAttachmentLimitExceed; @override @@ -1856,17 +1859,18 @@ class MessageInputState extends State { /// Adds an attachment to the [_attachments] map void _addAttachments(Iterable attachments) { + final limit = widget.attachmentLimit; final length = _attachments.length + attachments.length; - if (length > widget.attachmentLimit) { + if (length > limit) { final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; if (onAttachmentLimitExceed != null) { return onAttachmentLimitExceed( widget.attachmentLimit, - 'Attachment Limit crossed ${widget.attachmentLimit}', + context.translations.attachmentLimitExceedError(limit), ); } return _showErrorAlert( - 'Attachment Limit crossed ${widget.attachmentLimit}', + context.translations.attachmentLimitExceedError(limit), ); } for (final attachment in attachments) { 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 8a1d196e..8d2109f6 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -381,6 +381,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Reply to Message'; + + @override + String attachmentLimitExceedError(int limit) => + 'Attachment limit exceeded, limit: $limit'; } 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 5041d398..8c905b41 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 @@ -357,4 +357,8 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Reply to Message'; + + @override + String attachmentLimitExceedError(int limit) => + 'Attachment limit exceeded, limit: $limit'; } 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 b43e4ba8..10d9b2a7 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 @@ -362,4 +362,10 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Responder al Mensaje'; + + @override + String attachmentLimitExceedError(int limit) { + // TODO: implement attachmentLimitExceedError + throw UnimplementedError(); + } } 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 9d8586d5..17efdd70 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 @@ -361,4 +361,10 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'Répondre au Message'; + + @override + String attachmentLimitExceedError(int limit) { + // TODO: implement attachmentLimitExceedError + throw UnimplementedError(); + } } 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 850e481b..c08f77a2 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 @@ -356,4 +356,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get replyToMessageLabel => 'संदेश का जवाब'; + + @override + String attachmentLimitExceedError(int limit) { + // TODO: implement attachmentLimitExceedError + throw UnimplementedError(); + } } 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 8b2e1692..084878ee 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 @@ -358,4 +358,10 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; @override String get replyToMessageLabel => 'Rispondi al messaggio'; + + @override + String attachmentLimitExceedError(int limit) { + // TODO: implement attachmentLimitExceedError + throw UnimplementedError(); + } } diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart index 0e621159..f4eca409 100644 --- a/packages/stream_chat_localizations/test/translations_test.dart +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -177,6 +177,7 @@ void main() { expect(localizations.ofText, isNotNull); expect(localizations.fileText, isNotNull); expect(localizations.replyToMessageLabel, isNotNull); + expect(localizations.attachmentLimitExceedError(3), isNotNull); }); } From 6bbb80c2e9d15d5995b238116f0841ce0960129a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 16:45:30 +0530 Subject: [PATCH 03/12] fix(ui): add null check for message.text while parsing existing message. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/message_input.dart | 3 ++- 1 file changed, 2 insertions(+), 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 f7964c1e..d14e9a54 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -2259,7 +2259,8 @@ class MessageInputState extends State { } void _parseExistingMessage(Message message) { - _textEditingController.text = message.text!; + final messageText = message.text; + if (messageText != null) textEditingController.text = messageText; _addAttachments(message.attachments); } From d67ff91e1283ed37fbde6605681710b0892f27f5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 16:46:29 +0530 Subject: [PATCH 04/12] feat(ui): Focus message input if initial message is provided. Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/message_input.dart | 3 ++- 1 file changed, 2 insertions(+), 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 d14e9a54..967ec08e 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -2280,7 +2280,8 @@ class MessageInputState extends State { void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); _messageInputTheme = MessageInputTheme.of(context); - if (widget.editMessage != null && !_initialized) { + if ((widget.editMessage != null || widget.initialMessage != null) && + !_initialized) { FocusScope.of(context).requestFocus(_focusNode); _initialized = true; } From 0bdfff5bf6be54dbe9a9baa9b2c5d3e25a48fbd5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 16:49:03 +0530 Subject: [PATCH 05/12] fix(ui): disable camera and video button if attachment limit is crossed. Signed-off-by: xsahil03x --- .../lib/src/message_input.dart | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 967ec08e..329d9816 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1042,6 +1042,9 @@ class MessageInputState extends State { final _attachmentContainsFile = _attachments.values.any((it) => it.type == 'file'); + final attachmentLimitCrossed = + _attachments.length >= widget.attachmentLimit; + Color _getIconColor(int index) { final streamChatThemeData = _streamChatTheme; switch (index) { @@ -1061,15 +1064,21 @@ class MessageInputState extends State { : streamChatThemeData.colorTheme.textHighEmphasis .withOpacity(0.2)); case 2: - return _attachmentContainsFile && _attachments.isNotEmpty + return attachmentLimitCrossed ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); + : _attachmentContainsFile && _attachments.isNotEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); case 3: - return _attachmentContainsFile && _attachments.isNotEmpty + return attachmentLimitCrossed ? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2) - : streamChatThemeData.colorTheme.textHighEmphasis - .withOpacity(0.5); + : _attachmentContainsFile && _attachments.isNotEmpty + ? streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.2) + : streamChatThemeData.colorTheme.textHighEmphasis + .withOpacity(0.5); default: return Colors.black; } @@ -1112,7 +1121,8 @@ class MessageInputState extends State { icon: StreamSvgIcon.camera( color: _getIconColor(2), ), - onPressed: _attachmentContainsFile && _attachments.isNotEmpty + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && _attachments.isNotEmpty) ? null : () { pickFile(DefaultAttachmentTypes.image, camera: true); @@ -1123,7 +1133,8 @@ class MessageInputState extends State { icon: StreamSvgIcon.record( color: _getIconColor(3), ), - onPressed: _attachmentContainsFile && _attachments.isNotEmpty + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && _attachments.isNotEmpty) ? null : () { pickFile(DefaultAttachmentTypes.video, camera: true); From 9588daf683c01472601b0184c6c15a87e99e0bd5 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 16:49:52 +0530 Subject: [PATCH 06/12] chore(ui): apply review changes, minor ui improvements Signed-off-by: xsahil03x --- .../lib/src/message_input.dart | 175 ++++++++---------- 1 file changed, 80 insertions(+), 95 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 329d9816..bf304ec4 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -326,19 +326,18 @@ class MessageInputState extends State { bool _sendAsDm = false; bool _openFilePickerSection = false; int _filePickerIndex = 0; - double _filePickerSize = _kMinMediaPickerSize; - final KeyboardVisibilityController _keyboardVisibilityController = - KeyboardVisibilityController(); + + final _keyboardVisibilityController = KeyboardVisibilityController(); /// The editing controller passed to the input TextField - late final TextEditingController _textEditingController; + late final TextEditingController textEditingController; late StreamChatThemeData _streamChatTheme; late MessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => widget.quotedMessage != null; - bool get _messageIsPresent => _textEditingController.text.trim().isNotEmpty; + bool get _messageIsPresent => textEditingController.text.trim().isNotEmpty; late DateTime? _cooldownStartedAt; int? _timeOut; @@ -356,19 +355,19 @@ class MessageInputState extends State { _keyboardListener = _keyboardVisibilityController.onChange.listen((visible) { if (_focusNode.hasFocus) { - _onChanged(context, _textEditingController.text); + _onChanged(context, textEditingController.text); } }); } - _textEditingController = + textEditingController = widget.textEditingController ?? TextEditingController(); if (widget.editMessage != null || widget.initialMessage != null) { _parseExistingMessage(widget.editMessage ?? widget.initialMessage!); } - _textEditingController.addListener(() { - _onChanged(context, _textEditingController.text); + textEditingController.addListener(() { + _onChanged(context, textEditingController.text); }); _focusNode.addListener(() { @@ -660,7 +659,7 @@ class MessageInputState extends State { maxLines: null, onSubmitted: (_) => sendMessage(), keyboardType: widget.keyboardType, - controller: _textEditingController, + controller: textEditingController, focusNode: _focusNode, style: _messageInputTheme.inputTextStyle, autofocus: widget.autofocus, @@ -843,15 +842,15 @@ class MessageInputState extends State { void _checkEmoji(String s, BuildContext context) { if (s.isNotEmpty && - _textEditingController.selection.baseOffset > 0 && - _textEditingController.text + textEditingController.selection.baseOffset > 0 && + textEditingController.text .substring( 0, - _textEditingController.selection.baseOffset, + textEditingController.selection.baseOffset, ) .contains(':')) { - final textToSelection = _textEditingController.text - .substring(0, _textEditingController.value.selection.start); + 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); @@ -870,9 +869,9 @@ class MessageInputState extends State { void _checkMentions(String s, BuildContext context) { if (s.isNotEmpty && - _textEditingController.selection.baseOffset > 0 && - _textEditingController.text - .substring(0, _textEditingController.selection.baseOffset) + textEditingController.selection.baseOffset > 0 && + textEditingController.text + .substring(0, textEditingController.selection.baseOffset) .split(' ') .last .contains('@')) { @@ -895,7 +894,7 @@ class MessageInputState extends State { if (matchedCommandsList.length == 1) { _chosenCommand = matchedCommandsList[0]; - _textEditingController.clear(); + textEditingController.clear(); setState(() { _commandEnabled = true; }); @@ -911,7 +910,7 @@ class MessageInputState extends State { } OverlayEntry? _buildCommandsOverlayEntry() { - final text = _textEditingController.text.trimLeft(); + final text = textEditingController.text.trimLeft(); final commands = StreamChannel.of(context) .channel .config @@ -1086,7 +1085,7 @@ class MessageInputState extends State { return AnimatedContainer( duration: const Duration(milliseconds: 300), - height: _openFilePickerSection ? _filePickerSize : 0, + height: _openFilePickerSection ? _kMinMediaPickerSize : 0, child: Material( color: _streamChatTheme.colorTheme.inputBg, child: Column( @@ -1142,66 +1141,50 @@ class MessageInputState extends State { ), ], ), - GestureDetector( - onVerticalDragUpdate: (update) { - setState(() { - _filePickerSize = (_filePickerSize - update.delta.dy).clamp( - _kMinMediaPickerSize, - MediaQuery.of(context).size.height / 1.7, - ); - }); - }, - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), + DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), ), - child: SizedBox( - width: double.infinity, - child: Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: SizedBox( - width: 40, - height: 4, - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.inputBg, - borderRadius: BorderRadius.circular(4), - ), - ), - ), + ), + 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); - } - }, - ), + 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); + } + }, ), ), + ), ], ), ), @@ -1339,8 +1322,8 @@ class MessageInputState extends State { } OverlayEntry? _buildMentionsOverlayEntry() { - final splits = _textEditingController.text - .substring(0, _textEditingController.value.selection.start) + final splits = textEditingController.text + .substring(0, textEditingController.value.selection.start) .split('@'); final query = splits.last.toLowerCase(); @@ -1402,10 +1385,10 @@ class MessageInputState extends State { splits[splits.length - 1] = m.user!.name; final rejoin = splits.join('@'); - _textEditingController.value = TextEditingValue( + textEditingController.value = TextEditingValue( text: rejoin + - _textEditingController.text.substring( - _textEditingController.selection.start), + textEditingController.text.substring( + textEditingController.selection.start), selection: TextSelection.collapsed( offset: rejoin.length, ), @@ -1449,8 +1432,8 @@ class MessageInputState extends State { } OverlayEntry? _buildEmojiOverlay() { - final splits = _textEditingController.text - .substring(0, _textEditingController.value.selection.start) + final splits = textEditingController.text + .substring(0, textEditingController.value.selection.start) .split(':'); final query = splits.last.toLowerCase(); @@ -1561,10 +1544,10 @@ class MessageInputState extends State { void _chooseEmoji(List splits, Emoji emoji) { final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; - _textEditingController.value = TextEditingValue( + textEditingController.value = TextEditingValue( text: rejoin + - _textEditingController.text - .substring(_textEditingController.selection.start), + textEditingController.text + .substring(textEditingController.selection.start), selection: TextSelection.collapsed( offset: rejoin.length, ), @@ -1575,7 +1558,7 @@ class MessageInputState extends State { } void _setCommand(Command c) { - _textEditingController.clear(); + textEditingController.clear(); setState(() { _chosenCommand = c; _commandEnabled = true; @@ -1769,7 +1752,7 @@ class MessageInputState extends State { } Widget _buildCommandButton(BuildContext context) { - final s = _textEditingController.text.trim(); + final s = textEditingController.text.trim(); final defaultButton = IconButton( icon: StreamSvgIcon.lightning( color: s.isNotEmpty @@ -1786,10 +1769,7 @@ class MessageInputState extends State { splashRadius: 24, onPressed: () async { if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); + setState(() => _openFilePickerSection = false); await Future.delayed(const Duration(milliseconds: 300)); } @@ -1835,10 +1815,7 @@ class MessageInputState extends State { _mentionsOverlay = null; if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); + setState(() => _openFilePickerSection = false); } else { showAttachmentModal(); } @@ -1930,6 +1907,14 @@ 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) { + setState(() => _addAttachments([attachment])); + } + /// Adds an attachment to the [_attachments] map void _addAttachments(Iterable attachments) { final limit = widget.attachmentLimit; @@ -2110,7 +2095,7 @@ class MessageInputState extends State { /// Sends the current message Future sendMessage() async { - var text = _textEditingController.text.trim(); + var text = textEditingController.text.trim(); if (text.isEmpty && _attachments.isEmpty) { return; } @@ -2123,7 +2108,7 @@ class MessageInputState extends State { final attachments = [..._attachments.values]; - _textEditingController.clear(); + textEditingController.clear(); _attachments.clear(); widget.onQuotedMessageCleared?.call(); From 958ef8e9df088977649e8cd1f1ade11a95eafa54 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 17:33:48 +0530 Subject: [PATCH 07/12] fix(ui): add 8 left padding to textInput if command is enabled. Signed-off-by: xsahil03x --- 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 bf304ec4..d2b94a29 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -625,7 +625,7 @@ class MessageInputState extends State { final margin = (widget.sendButtonLocation == SendButtonLocation.inside ? const EdgeInsets.only(right: 8) : EdgeInsets.zero) + - (widget.actionsLocation != ActionsLocation.left + (widget.actionsLocation != ActionsLocation.left || _commandEnabled ? const EdgeInsets.only(left: 8) : EdgeInsets.zero); return Expanded( From 55853c2acab2756ab295eaaa0b4daff3bf5d57de Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 23 Aug 2021 17:42:29 +0530 Subject: [PATCH 08/12] refactor(ui): remove redundant web pickers from MessageInput Signed-off-by: xsahil03x --- .../lib/src/message_input.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index d2b94a29..84d67dea 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1875,24 +1875,6 @@ class MessageInputState extends State { Navigator.pop(context); }, ), - if (!kIsWeb) - ListTile( - leading: const Icon(Icons.camera_alt), - title: Text(context.translations.photoFromCameraLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.image, camera: true); - Navigator.pop(context); - }, - ), - if (!kIsWeb) - ListTile( - leading: const Icon(Icons.videocam), - title: Text(context.translations.videoFromCameraLabel), - onTap: () { - pickFile(DefaultAttachmentTypes.video, camera: true); - Navigator.pop(context); - }, - ), ListTile( leading: const Icon(Icons.insert_drive_file), title: Text(context.translations.uploadAFileLabel), From a37c92cdbf64afe88a019ee9ce6e1a50d1191bc2 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 23 Aug 2021 20:46:14 +0530 Subject: [PATCH 09/12] fix test --- .../lib/src/message_input.dart | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 84d67dea..4227c90d 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1163,28 +1163,29 @@ class MessageInputState extends State { ), ), ), - 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); - } - }, + 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); + } + }, + ), ), ), - ), ], ), ), From 93f60bc2484302ecad69d6438f87920fbe5ae1a7 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 23 Aug 2021 20:51:26 +0530 Subject: [PATCH 10/12] added comment --- .../stream_chat_flutter/lib/src/localization/translations.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index cf76ea0b..e86893e9 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -309,6 +309,8 @@ abstract class Translations { /// The label for "Reply to message" String get replyToMessageLabel; + /// Label for "Attachment limit exceeded: + /// it's not possible to add more than $limit attachments" String attachmentLimitExceedError(int limit); } From f0976f0aab1a77615d2c2cfe49237ec3f93c060c Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 23 Aug 2021 21:00:04 +0530 Subject: [PATCH 11/12] added comment --- packages/stream_chat_localizations/example/lib/add_new_lang.dart | 1 + 1 file changed, 1 insertion(+) 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 364761a2..28ad50fb 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -388,6 +388,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String attachmentLimitExceedError(int limit) => 'Attachment limit exceeded, limit: $limit'; + @override String get slowModeOnLabel => 'Slow mode ON'; } From 02066e3bfb617199b656d337e492700f0815e0a1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 24 Aug 2021 09:46:29 +0200 Subject: [PATCH 12/12] feat(localization): add localized strings --- .../lib/src/localization/translations.dart | 6 +++--- .../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 | 7 +++---- .../lib/src/stream_chat_localizations_ko.dart | 6 ++---- 7 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index e86893e9..246cd9bf 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -676,9 +676,9 @@ class DefaultTranslations implements Translations { String get replyToMessageLabel => 'Reply to Message'; @override - String attachmentLimitExceedError(int limit) => - 'Attachment limit exceeded, limit: $limit'; + String get slowModeOnLabel => 'Slow mode ON'; @override - String get slowModeOnLabel => 'Slow mode ON'; + String attachmentLimitExceedError(int limit) => """ +Attachment limit exceeded: it's not possible to add more than $limit attachments"""; } 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 aac8f1eb..69ee9237 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 @@ -366,10 +366,9 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Responder al Mensaje'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +No es posible añadir más de $limit archivos adjuntos + '''; @override String get slowModeOnLabel => 'Modo lento activado'; 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 ce8debf7..0727075a 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 @@ -365,10 +365,9 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Répondre au Message'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $limit pièces jointes + '''; @override String get slowModeOnLabel => 'Mode lent activé'; 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 e80979c3..ea1738de 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 @@ -360,10 +360,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'संदेश का जवाब'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +अटैचमेंट लिमिट: $limit अटैचमेंट से अधिक जोड़ना संभव नहीं है + '''; @override String get slowModeOnLabel => 'स्लो मोड चालू'; 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 abe2b3a8..64e96dc0 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 @@ -362,10 +362,9 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get replyToMessageLabel => 'Rispondi al messaggio'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +Attenzione: il limite massimo di $limit file è stato superato. + '''; @override String get slowModeOnLabel => 'Slowmode attiva'; 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 533b84fd..8b58630d 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 @@ -351,8 +351,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get slowModeOnLabel => 'スローモードオン'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => ''' +添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません + '''; } 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 9a3a1315..a52a914f 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 @@ -349,8 +349,6 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get slowModeOnLabel => '슬로모드 켜짐'; @override - String attachmentLimitExceedError(int limit) { - // TODO: implement attachmentLimitExceedError - throw UnimplementedError(); - } + String attachmentLimitExceedError(int limit) => + '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; }