From c62c65ade50305a03a9bd90e253baae57a414b67 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 2 Nov 2020 15:46:35 +0100 Subject: [PATCH 01/11] add emoji picker --- lib/src/message_input.dart | 185 ++++++++++++++++++++++++++++--------- pubspec.yaml | 2 + 2 files changed, 145 insertions(+), 42 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 7bdbfd5d..be987950 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1,11 +1,14 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:math'; 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_emoji/flutter_emoji.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:fuzzy/fuzzy.dart'; import 'package:http_parser/http_parser.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; @@ -167,7 +170,8 @@ class MessageInputState extends State { bool _messageIsPresent = false; bool _typingStarted = false; bool _commandEnabled = false; - OverlayEntry _commandsOverlay, _mentionsOverlay; + OverlayEntry _commandsOverlay, _mentionsOverlay, _emojiOverlay; + Fuzzy _emojiFuse; Command _chosenCommand; bool _actionsShrunk = false; @@ -319,6 +323,8 @@ class MessageInputState extends State { _commandsOverlay = null; _mentionsOverlay?.remove(); _mentionsOverlay = null; + _emojiOverlay?.remove(); + _emojiOverlay = null; if (s.startsWith('/')) { var matchedCommandsList = StreamChannel.of(context) @@ -354,6 +360,23 @@ class MessageInputState extends State { _mentionsOverlay = _buildMentionsOverlayEntry(); Overlay.of(context).insert(_mentionsOverlay); } + + if (textEditingController.selection.isCollapsed && + (s[textEditingController.selection.start - 1] == ':' || + textEditingController.text + .substring( + 0, + textEditingController.selection.start, + ) + .split(' ') + .last + .contains(':'))) { + _emojiOverlay = _buildEmojiOverlay(); + + if (_emojiOverlay != null) { + Overlay.of(context).insert(_emojiOverlay); + } + } }, onTap: () { setState(() { @@ -419,40 +442,6 @@ class MessageInputState extends State { ); } - Positioned _buildBorder(BuildContext context) { - return Positioned.fill( - child: Container( - width: MediaQuery.of(context).size.width, - padding: EdgeInsets.all(2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - gradient: _getGradient(context), - ), - child: Container( - decoration: BoxDecoration( - color: StreamChatTheme.of(context) - .channelTheme - .inputBackground - .withAlpha(255), - borderRadius: BorderRadius.circular(10.0), - ), - child: Container( - decoration: BoxDecoration( - color: StreamChatTheme.of(context).channelTheme.inputBackground, - borderRadius: BorderRadius.circular(10.0), - border: Border.all( - color: _typingStarted - ? Colors.transparent - : Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.2) - : Colors.black.withOpacity(.2)), - ), - ), - ), - ), - ); - } - OverlayEntry _buildCommandsOverlayEntry() { final text = textEditingController.text; final commands = StreamChannel.of(context) @@ -482,13 +471,6 @@ class MessageInputState extends State { child: Container( constraints: BoxConstraints.loose(Size.fromHeight(400)), decoration: BoxDecoration( - // boxShadow: [ - // BoxShadow( - // spreadRadius: -8, - // blurRadius: 5.0, - // offset: Offset(0, -4), - // ), - // ], color: StreamChatTheme.of(context).primaryColor, borderRadius: BorderRadius.circular(8.0)), child: ListView( @@ -664,6 +646,105 @@ class MessageInputState extends State { }); } + OverlayEntry _buildEmojiOverlay() { + final splits = textEditingController.text + .substring(0, textEditingController.value.selection.start) + .split(':'); + final query = splits.last.toLowerCase(); + + if (query.isEmpty) { + return null; + } + + final parser = EmojiParser(); + final emojis = + _emojiFuse.search(query).map((e) => parser.get(e.item)).toList(); + + RenderBox renderBox = context.findRenderObject(); + final size = renderBox.size; + + return OverlayEntry(builder: (context) { + return Positioned( + bottom: size.height + MediaQuery.of(context).viewInsets.bottom, + left: 0, + right: 0, + child: Card( + margin: EdgeInsets.all(8.0), + elevation: 2.0, + color: StreamChatTheme.of(context).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + clipBehavior: Clip.antiAlias, + child: Container( + constraints: BoxConstraints.loose(Size.fromHeight(200)), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + spreadRadius: -8, + blurRadius: 5.0, + offset: Offset(0, -4), + ), + ], + color: StreamChatTheme.of(context).primaryColor, + ), + child: ListView( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0, top: 8.0), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Icon( + StreamIcons.smile, + color: StreamChatTheme.of(context).accentColor, + ), + ), + Text( + 'Emoji matching "$query"', + style: TextStyle( + color: Colors.black.withOpacity(.5), + ), + ) + ], + ), + ), + ...emojis.map((emoji) => ListTile( + title: Text( + "${emoji.code} ${emoji.name.replaceAll('_', ' ')}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.5, + ), + ), + onTap: () { + splits[splits.length - 1] = emoji.code; + final rejoin = splits.join(''); + + textEditingController.value = TextEditingValue( + text: rejoin + + textEditingController.text.substring( + textEditingController.selection.start), + selection: TextSelection.collapsed( + offset: rejoin.length, + ), + ); + + _emojiOverlay?.remove(); + _emojiOverlay = null; + }, + )) + ], + ), + ), + ), + ); + }); + } + void _setCommand(Command c) { textEditingController.clear(); setState(() { @@ -1217,6 +1298,13 @@ class MessageInputState extends State { @override void initState() { super.initState(); + Map emojiMap = jsonDecode(EmojiParser.JSON_EMOJI); + + _emojiFuse = Fuzzy(emojiMap.keys.toList(), + options: FuzzyOptions( + matchAllTokens: true, + tokenize: true, + )); if (!kIsWeb) { _keyboardListener = KeyboardVisibility.onChange.listen((visible) { @@ -1238,6 +1326,15 @@ class MessageInputState extends State { }); } } + + if (_emojiOverlay != null) { + if (textEditingController.text.contains('@')) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _emojiOverlay = _buildEmojiOverlay(); + Overlay.of(context).insert(_emojiOverlay); + }); + } + } } else { if (_commandsOverlay != null) { _commandsOverlay.remove(); @@ -1245,6 +1342,9 @@ class MessageInputState extends State { if (_mentionsOverlay != null) { _mentionsOverlay.remove(); } + if (_emojiOverlay != null) { + _emojiOverlay.remove(); + } } }); } @@ -1273,6 +1373,7 @@ class MessageInputState extends State { @override void dispose() { _commandsOverlay?.remove(); + _emojiOverlay?.remove(); _mentionsOverlay?.remove(); _keyboardListener?.cancel(); super.dispose(); diff --git a/pubspec.yaml b/pubspec.yaml index 452eb69a..1dfb195f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,9 @@ dependencies: image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.2.1 stream_chat: ^0.2.10+1 + flutter_emoji: ^2.2.1+1 mime: ^0.9.6+3 + fuzzy: ^0.2.3 visibility_detector: ^0.1.5 http_parser: ^3.1.4 flutter_slidable: ^0.5.4 From b0897d9cf017fb18c019bd9a4d736117cf5e1497 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 3 Nov 2020 10:09:30 +0100 Subject: [PATCH 02/11] show big emoji --- lib/src/message_widget.dart | 98 +++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index e65da576..2790f3c6 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter_emoji/flutter_emoji.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/message_actions_modal.dart'; @@ -717,37 +718,69 @@ class _MessageWidgetState extends State { return SizedBox(); } + Widget _wrapTextInBubble({ + BuildContext context, + Widget child, + }) { + return Material( + shape: widget.shape ?? + RoundedRectangleBorder( + side: widget.borderSide ?? + BorderSide( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withAlpha(24) + : Colors.black.withAlpha(24), + ), + borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, + ), + color: _getBackgroundColor(), + child: child, + ); + } + Widget _buildTextBubble(BuildContext context) { + final emojiParser = EmojiParser(); + final isOnlyEmoji = + widget.message.text.characters.every((c) => emojiParser.hasEmoji(c)); + + Widget child = Transform( + transform: Matrix4.rotationY(widget.reverse ? pi : 0), + alignment: Alignment.center, + child: Padding( + padding: widget.textPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + getFailedMessageWidget(context), + widget.textBuilder != null + ? widget.textBuilder(context, widget.message) + : MessageText( + onLinkTap: widget.onLinkTap, + message: widget.message, + onMentionTap: widget.onMentionTap, + messageTheme: isOnlyEmoji + ? widget.messageTheme.copyWith( + messageText: + widget.messageTheme.messageText.copyWith( + fontSize: 40, + )) + : widget.messageTheme, + ), + ], + ), + ), + ); + + if (!isOnlyEmoji) { + child = _wrapTextInBubble( + context: context, + child: child, + ); + } return GestureDetector( onTap: () => retryMessage(context), onLongPress: () => onLongPress(context), - child: Material( - shape: widget.shape ?? - RoundedRectangleBorder( - side: widget.borderSide ?? - BorderSide( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withAlpha(24) - : Colors.black.withAlpha(24), - ), - borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, - ), - color: _getBackgroundColor(), - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Padding( - padding: widget.textPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - getFailedMessageWidget(context), - _buildText(context), - ], - ), - ), - ), - ), + child: child, ); } @@ -781,15 +814,4 @@ class _MessageWidgetState extends State { return; } } - - Widget _buildText(BuildContext context) { - return widget.textBuilder != null - ? widget.textBuilder(context, widget.message) - : MessageText( - onLinkTap: widget.onLinkTap, - message: widget.message, - onMentionTap: widget.onMentionTap, - messageTheme: widget.messageTheme, - ); - } } From 5e714c93d00f616ec2a51b0eff5d5def73bc6ccd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 3 Nov 2020 10:25:25 +0100 Subject: [PATCH 03/11] replace emoji after typing: --- lib/src/message_input.dart | 152 ++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 63 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index be987950..4f56827e 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -326,57 +326,11 @@ class MessageInputState extends State { _emojiOverlay?.remove(); _emojiOverlay = null; - if (s.startsWith('/')) { - var matchedCommandsList = StreamChannel.of(context) - .channel - .config - .commands - .where((element) => element.name == s.substring(1)) - .toList(); + _checkCommands(s, context); - if (matchedCommandsList.length == 1) { - _chosenCommand = matchedCommandsList[0]; - textEditingController.clear(); - _messageIsPresent = false; - setState(() { - _commandEnabled = true; - }); - _commandsOverlay.remove(); - _commandsOverlay = null; - } else { - _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); - } - } + _checkMentions(s, context); - if (textEditingController.selection.isCollapsed && - (s[textEditingController.selection.start - 1] == '@' || - textEditingController.text - .substring( - 0, textEditingController.selection.start) - .split(' ') - .last - .contains('@'))) { - _mentionsOverlay = _buildMentionsOverlayEntry(); - Overlay.of(context).insert(_mentionsOverlay); - } - - if (textEditingController.selection.isCollapsed && - (s[textEditingController.selection.start - 1] == ':' || - textEditingController.text - .substring( - 0, - textEditingController.selection.start, - ) - .split(' ') - .last - .contains(':'))) { - _emojiOverlay = _buildEmojiOverlay(); - - if (_emojiOverlay != null) { - Overlay.of(context).insert(_emojiOverlay); - } - } + _checkEmoji(s, context); }, onTap: () { setState(() { @@ -442,6 +396,74 @@ class MessageInputState extends State { ); } + void _checkEmoji(String s, BuildContext context) { + if (textEditingController.selection.isCollapsed && + (s[textEditingController.selection.start - 1] == ':' || + textEditingController.text + .substring( + 0, + textEditingController.selection.start, + ) + .split(' ') + .last + .contains(':'))) { + final emojiParser = EmojiParser(); + final textToSelection = textEditingController.text + .substring(0, textEditingController.value.selection.start); + final splits = textToSelection.split(':'); + final query = splits[1]?.toLowerCase(); + final emoji = emojiParser.get(query); + + if (textToSelection.endsWith(':') && emoji.name != '') { + _chooseEmoji(splits, emoji); + } else { + _emojiOverlay = _buildEmojiOverlay(); + + if (_emojiOverlay != null) { + Overlay.of(context).insert(_emojiOverlay); + } + } + } + } + + void _checkMentions(String s, BuildContext context) { + if (textEditingController.selection.isCollapsed && + (s[textEditingController.selection.start - 1] == '@' || + textEditingController.text + .substring(0, textEditingController.selection.start) + .split(' ') + .last + .contains('@'))) { + _mentionsOverlay = _buildMentionsOverlayEntry(); + Overlay.of(context).insert(_mentionsOverlay); + } + } + + void _checkCommands(String s, BuildContext context) { + if (s.startsWith('/')) { + var matchedCommandsList = StreamChannel.of(context) + .channel + .config + .commands + .where((element) => element.name == s.substring(1)) + .toList(); + + if (matchedCommandsList.length == 1) { + _chosenCommand = matchedCommandsList[0]; + textEditingController.clear(); + _messageIsPresent = false; + setState(() { + _commandEnabled = true; + }); + _commandsOverlay.remove(); + _commandsOverlay = null; + } else { + _commandsOverlay = _buildCommandsOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + } + } + } + OverlayEntry _buildCommandsOverlayEntry() { final text = textEditingController.text; final commands = StreamChannel.of(context) @@ -721,20 +743,7 @@ class MessageInputState extends State { ), ), onTap: () { - splits[splits.length - 1] = emoji.code; - final rejoin = splits.join(''); - - textEditingController.value = TextEditingValue( - text: rejoin + - textEditingController.text.substring( - textEditingController.selection.start), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); - - _emojiOverlay?.remove(); - _emojiOverlay = null; + _chooseEmoji(splits, emoji); }, )) ], @@ -745,6 +754,23 @@ class MessageInputState extends State { }); } + void _chooseEmoji(List splits, Emoji emoji) { + splits[1] = emoji.code; + final rejoin = splits.join(''); + + textEditingController.value = TextEditingValue( + text: rejoin + + textEditingController.text + .substring(textEditingController.selection.start), + selection: TextSelection.collapsed( + offset: rejoin.length, + ), + ); + + _emojiOverlay?.remove(); + _emojiOverlay = null; + } + void _setCommand(Command c) { textEditingController.clear(); setState(() { From 4bb5598c71f47316330bede3b8b70783fcbed4b1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 5 Nov 2020 11:35:24 +0100 Subject: [PATCH 04/11] use another emoji package --- lib/src/message_input.dart | 37 ++++++++++++++++++------------------- lib/src/message_widget.dart | 5 ++--- pubspec.yaml | 2 +- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 4f56827e..8da205b9 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1,12 +1,11 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:math'; +import 'package:emojis/emoji.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_emoji/flutter_emoji.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:fuzzy/fuzzy.dart'; import 'package:http_parser/http_parser.dart'; @@ -171,7 +170,7 @@ class MessageInputState extends State { bool _typingStarted = false; bool _commandEnabled = false; OverlayEntry _commandsOverlay, _mentionsOverlay, _emojiOverlay; - Fuzzy _emojiFuse; + Fuzzy _emojiFuse; Command _chosenCommand; bool _actionsShrunk = false; @@ -326,7 +325,7 @@ class MessageInputState extends State { _emojiOverlay?.remove(); _emojiOverlay = null; - _checkCommands(s, context); + _checkCommands(s.trimLeft(), context); _checkMentions(s, context); @@ -398,7 +397,7 @@ class MessageInputState extends State { void _checkEmoji(String s, BuildContext context) { if (textEditingController.selection.isCollapsed && - (s[textEditingController.selection.start - 1] == ':' || + (s.isNotEmpty && s[textEditingController.selection.start - 1] == ':' || textEditingController.text .substring( 0, @@ -407,14 +406,13 @@ class MessageInputState extends State { .split(' ') .last .contains(':'))) { - final emojiParser = EmojiParser(); final textToSelection = textEditingController.text .substring(0, textEditingController.value.selection.start); final splits = textToSelection.split(':'); final query = splits[1]?.toLowerCase(); - final emoji = emojiParser.get(query); + final emoji = Emoji.byName(query); - if (textToSelection.endsWith(':') && emoji.name != '') { + if (textToSelection.endsWith(':') && emoji != null) { _chooseEmoji(splits, emoji); } else { _emojiOverlay = _buildEmojiOverlay(); @@ -428,7 +426,7 @@ class MessageInputState extends State { void _checkMentions(String s, BuildContext context) { if (textEditingController.selection.isCollapsed && - (s[textEditingController.selection.start - 1] == '@' || + (s.isNotEmpty && s[textEditingController.selection.start - 1] == '@' || textEditingController.text .substring(0, textEditingController.selection.start) .split(' ') @@ -465,7 +463,7 @@ class MessageInputState extends State { } OverlayEntry _buildCommandsOverlayEntry() { - final text = textEditingController.text; + final text = textEditingController.text.trimLeft(); final commands = StreamChannel.of(context) .channel .config @@ -678,9 +676,11 @@ class MessageInputState extends State { return null; } - final parser = EmojiParser(); - final emojis = - _emojiFuse.search(query).map((e) => parser.get(e.item)).toList(); + final emojis = _emojiFuse + .search(query) + .map((e) => Emoji.byName(e.item)) + .where((e) => e != null) + .toList(); RenderBox renderBox = context.findRenderObject(); final size = renderBox.size; @@ -736,7 +736,7 @@ class MessageInputState extends State { ), ...emojis.map((emoji) => ListTile( title: Text( - "${emoji.code} ${emoji.name.replaceAll('_', ' ')}", + "${emoji.char} ${emoji.name.replaceAll('_', ' ')}", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14.5, @@ -755,7 +755,7 @@ class MessageInputState extends State { } void _chooseEmoji(List splits, Emoji emoji) { - splits[1] = emoji.code; + splits[1] = emoji.char; final rejoin = splits.join(''); textEditingController.value = TextEditingValue( @@ -1324,9 +1324,8 @@ class MessageInputState extends State { @override void initState() { super.initState(); - Map emojiMap = jsonDecode(EmojiParser.JSON_EMOJI); - _emojiFuse = Fuzzy(emojiMap.keys.toList(), + _emojiFuse = Fuzzy(Emoji.all().map((e) => e.name).toList(), options: FuzzyOptions( matchAllTokens: true, tokenize: true, @@ -1336,7 +1335,7 @@ class MessageInputState extends State { _keyboardListener = KeyboardVisibility.onChange.listen((visible) { if (visible) { if (_commandsOverlay != null) { - if (textEditingController.text.startsWith('/')) { + if (textEditingController.text.trimLeft().startsWith('/')) { WidgetsBinding.instance.addPostFrameCallback((_) { _commandsOverlay = _buildCommandsOverlayEntry(); Overlay.of(context).insert(_commandsOverlay); @@ -1354,7 +1353,7 @@ class MessageInputState extends State { } if (_emojiOverlay != null) { - if (textEditingController.text.contains('@')) { + if (textEditingController.text.contains(':')) { WidgetsBinding.instance.addPostFrameCallback((_) { _emojiOverlay = _buildEmojiOverlay(); Overlay.of(context).insert(_emojiOverlay); diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index d516aa1e..476217f3 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -1,11 +1,11 @@ import 'dart:math'; import 'dart:ui'; +import 'package:emojis/emoji.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_emoji/flutter_emoji.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/message_actions_modal.dart'; @@ -751,9 +751,8 @@ class _MessageWidgetState extends State { } Widget _buildTextBubble(BuildContext context) { - final emojiParser = EmojiParser(); final isOnlyEmoji = - widget.message.text.characters.every((c) => emojiParser.hasEmoji(c)); + widget.message.text.characters.every((c) => Emoji.byChar(c) != null); Widget child = Transform( transform: Matrix4.rotationY(widget.reverse ? pi : 0), diff --git a/pubspec.yaml b/pubspec.yaml index 1dfb195f..6aa8aff3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.2.1 stream_chat: ^0.2.10+1 - flutter_emoji: ^2.2.1+1 + emojis: ^0.9.3 mime: ^0.9.6+3 fuzzy: ^0.2.3 visibility_detector: ^0.1.5 From 3a5ae9af019870edc957f9d083370d32005d02d1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 5 Nov 2020 16:05:39 +0100 Subject: [PATCH 05/11] hide overlay if there is no match --- lib/src/message_input.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 8da205b9..2be0cf6a 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -682,6 +682,10 @@ class MessageInputState extends State { .where((e) => e != null) .toList(); + if (emojis.isEmpty) { + return null; + } + RenderBox renderBox = context.findRenderObject(); final size = renderBox.size; From 7de9206e60362ed0e660cba40b9620782a947efc Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 5 Nov 2020 16:12:54 +0100 Subject: [PATCH 06/11] use listview.builder --- lib/src/message_input.dart | 77 ++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 2be0cf6a..1acdd64b 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -714,44 +714,49 @@ class MessageInputState extends State { ], color: StreamChatTheme.of(context).primaryColor, ), - child: ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0, top: 8.0), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Icon( - StreamIcons.smile, - color: StreamChatTheme.of(context).accentColor, - ), + child: ListView.builder( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + itemCount: emojis.length + 1, + itemBuilder: (context, i) { + if (i == 0) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, top: 8.0), + child: Row( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: Icon( + StreamIcons.smile, + color: StreamChatTheme.of(context).accentColor, + ), + ), + Text( + 'Emoji matching "$query"', + style: TextStyle( + color: Colors.black.withOpacity(.5), + ), + ) + ], ), - Text( - 'Emoji matching "$query"', - style: TextStyle( - color: Colors.black.withOpacity(.5), - ), - ) - ], - ), - ), - ...emojis.map((emoji) => ListTile( - title: Text( - "${emoji.char} ${emoji.name.replaceAll('_', ' ')}", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.5, - ), + ); + } + + final emoji = emojis[i - 1]; + return ListTile( + title: Text( + "${emoji.char} ${emoji.name.replaceAll('_', ' ')}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.5, ), - onTap: () { - _chooseEmoji(splits, emoji); - }, - )) - ], - ), + ), + onTap: () { + _chooseEmoji(splits, emoji); + }, + ); + }), ), ), ); From 0428119c8ab61847576f848abaf80294fb9d6e18 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 6 Nov 2020 10:31:39 +0100 Subject: [PATCH 07/11] fix performance and highlight text --- example/ios/Flutter/.last_build_id | 2 +- example/pubspec.yaml | 2 +- lib/src/message_input.dart | 38 +++++++++++++----------------- pubspec.yaml | 2 +- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index 5d3809be..8aba7787 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -bdf1751976c4ee1e2ef7638eabcfd1ea \ No newline at end of file +bb5f9103d9045cd6244bcae1f5f343e5 \ No newline at end of file diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 842f471c..97971445 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.39+41 +version: 1.0.40+42 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 1acdd64b..b2d124da 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -7,7 +7,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:fuzzy/fuzzy.dart'; import 'package:http_parser/http_parser.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; @@ -15,6 +14,7 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; +import 'package:substring_highlight/substring_highlight.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; @@ -170,7 +170,7 @@ class MessageInputState extends State { bool _typingStarted = false; bool _commandEnabled = false; OverlayEntry _commandsOverlay, _mentionsOverlay, _emojiOverlay; - Fuzzy _emojiFuse; + Iterable _emojiNames; Command _chosenCommand; bool _actionsShrunk = false; @@ -403,8 +403,6 @@ class MessageInputState extends State { 0, textEditingController.selection.start, ) - .split(' ') - .last .contains(':'))) { final textToSelection = textEditingController.text .substring(0, textEditingController.value.selection.start); @@ -676,11 +674,8 @@ class MessageInputState extends State { return null; } - final emojis = _emojiFuse - .search(query) - .map((e) => Emoji.byName(e.item)) - .where((e) => e != null) - .toList(); + final emojis = + _emojiNames.where((e) => e.contains(query)).map((e) => Emoji.byName(e)); if (emojis.isEmpty) { return null; @@ -743,14 +738,19 @@ class MessageInputState extends State { ); } - final emoji = emojis[i - 1]; + final emoji = emojis.elementAt(i - 1); return ListTile( - title: Text( - "${emoji.char} ${emoji.name.replaceAll('_', ' ')}", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.5, - ), + title: SubstringHighlight( + text: "${emoji.char} ${emoji.name.replaceAll('_', ' ')}", + term: query, + textStyleHighlight: + Theme.of(context).textTheme.headline6.copyWith( + fontSize: 14.5, + fontWeight: FontWeight.bold, + ), + textStyle: Theme.of(context).textTheme.headline6.copyWith( + fontSize: 14.5, + ), ), onTap: () { _chooseEmoji(splits, emoji); @@ -1334,11 +1334,7 @@ class MessageInputState extends State { void initState() { super.initState(); - _emojiFuse = Fuzzy(Emoji.all().map((e) => e.name).toList(), - options: FuzzyOptions( - matchAllTokens: true, - tokenize: true, - )); + _emojiNames = Emoji.all().map((e) => e.name); if (!kIsWeb) { _keyboardListener = KeyboardVisibility.onChange.listen((visible) { diff --git a/pubspec.yaml b/pubspec.yaml index 6aa8aff3..eb9f0545 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,9 +27,9 @@ dependencies: stream_chat: ^0.2.10+1 emojis: ^0.9.3 mime: ^0.9.6+3 - fuzzy: ^0.2.3 visibility_detector: ^0.1.5 http_parser: ^3.1.4 + substring_highlight: ^0.1.2 flutter_slidable: ^0.5.4 carousel_slider: ^2.2.1 clipboard: ^0.1.2+8 From df9b5100b8c6e4b45d912df8593abb2b3594296a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 6 Nov 2020 11:25:51 +0100 Subject: [PATCH 08/11] filter null emojis --- lib/src/message_input.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index b2d124da..e8e5900a 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -674,8 +674,10 @@ class MessageInputState extends State { return null; } - final emojis = - _emojiNames.where((e) => e.contains(query)).map((e) => Emoji.byName(e)); + final emojis = _emojiNames + .where((e) => e.contains(query)) + .map((e) => Emoji.byName(e)) + .where((e) => e != null); if (emojis.isEmpty) { return null; From 7463b4f7258168588d67bd479f5d38257d00e1db Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 6 Nov 2020 11:31:23 +0100 Subject: [PATCH 09/11] fix text overflow --- lib/src/message_input.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index e8e5900a..5cfff4e1 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -729,10 +729,12 @@ class MessageInputState extends State { color: StreamChatTheme.of(context).accentColor, ), ), - Text( - 'Emoji matching "$query"', - style: TextStyle( - color: Colors.black.withOpacity(.5), + Flexible( + child: Text( + 'Emoji matching "$query"', + style: TextStyle( + color: Colors.black.withOpacity(.5), + ), ), ) ], From b4646231fc4faba0872dd6d206e20aeb37341f64 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 6 Nov 2020 12:02:23 +0100 Subject: [PATCH 10/11] fix emoji picker with multiple : --- lib/src/message_input.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 5cfff4e1..90fcfd11 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -768,8 +768,7 @@ class MessageInputState extends State { } void _chooseEmoji(List splits, Emoji emoji) { - splits[1] = emoji.char; - final rejoin = splits.join(''); + final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char; textEditingController.value = TextEditingValue( text: rejoin + From 3aababa282ed18063302d8f4cb7feda92689d3ad Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 6 Nov 2020 12:20:21 +0100 Subject: [PATCH 11/11] automatically replace emoji --- lib/src/message_input.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 90fcfd11..7ef2ebd6 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -407,11 +407,11 @@ class MessageInputState extends State { final textToSelection = textEditingController.text .substring(0, textEditingController.value.selection.start); final splits = textToSelection.split(':'); - final query = splits[1]?.toLowerCase(); + final query = splits[splits.length - 2]?.toLowerCase(); final emoji = Emoji.byName(query); if (textToSelection.endsWith(':') && emoji != null) { - _chooseEmoji(splits, emoji); + _chooseEmoji(splits.sublist(0, splits.length - 1), emoji); } else { _emojiOverlay = _buildEmojiOverlay();