From 3ccba44302e63f10fca2c227ca22e0250b749132 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 27 Aug 2024 13:48:12 +0100 Subject: [PATCH 1/7] Back pressed on empty emoji closes picker --- app/lib/features/chat/widgets/custom_input.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/lib/features/chat/widgets/custom_input.dart b/app/lib/features/chat/widgets/custom_input.dart index 8165a7f2d3a7..ea8e05595407 100644 --- a/app/lib/features/chat/widgets/custom_input.dart +++ b/app/lib/features/chat/widgets/custom_input.dart @@ -309,6 +309,11 @@ class __ChatInputState extends ConsumerState<_ChatInput> { } void handleBackspacePressed() { + if (textController.text.isEmpty) { + // nothing left to clear, close the emoji picker + ref.read(chatInputProvider.notifier).emojiPickerVisible(false); + return; + } final newValue = textController.text.characters.skipLast(1).string; textController.text = newValue; } From 9df27b08fa0f8b0cde7d51bc42dd59560dea11ab Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 27 Aug 2024 13:48:37 +0100 Subject: [PATCH 2/7] Fix bug that emoji doesn't show if input never had focus --- app/lib/features/chat/widgets/custom_input.dart | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/lib/features/chat/widgets/custom_input.dart b/app/lib/features/chat/widgets/custom_input.dart index ea8e05595407..4046bb7fd1a4 100644 --- a/app/lib/features/chat/widgets/custom_input.dart +++ b/app/lib/features/chat/widgets/custom_input.dart @@ -279,14 +279,23 @@ class __ChatInputState extends ConsumerState<_ChatInput> { } void handleEmojiSelected(Category? category, Emoji emoji) { + String suffixText = ''; + + // Get the left text of cursor + String prefixText = ''; // Get cursor current position var cursorPos = textController.selection.base.offset; + if (cursorPos >= 0) { + // can be -1 on empty and never accessed - // Right text of cursor position - String suffixText = textController.text.substring(cursorPos); + // Right text of cursor position + suffixText = textController.text.substring(cursorPos); - // Get the left text of cursor - String prefixText = textController.text.substring(0, cursorPos); + // Get the left text of cursor + prefixText = textController.text.substring(0, cursorPos); + } else { + cursorPos = 0; + } int emojiLength = emoji.emoji.length; From b879bfde6ad32e98eb142cd71c04ea4c88300281 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 27 Aug 2024 13:49:27 +0100 Subject: [PATCH 3/7] Fix faulty emoji popup border colors --- app/lib/common/widgets/emoji_picker_widget.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/lib/common/widgets/emoji_picker_widget.dart b/app/lib/common/widgets/emoji_picker_widget.dart index e1b339106e93..8dcb63f60bfa 100644 --- a/app/lib/common/widgets/emoji_picker_widget.dart +++ b/app/lib/common/widgets/emoji_picker_widget.dart @@ -31,9 +31,10 @@ class EmojiPickerWidget extends StatelessWidget { ? const EdgeInsets.only(top: 10, left: 15, right: 15) : null, decoration: withBoarder - ? const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ? BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(16)), ) : null, height: height, From deba22e04bfe6f5a952dffa523e611349d86bdc9 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 27 Aug 2024 13:49:56 +0100 Subject: [PATCH 4/7] Fix Emoji Picker UX with a custom 'category picker' and 'search view' --- .../common/widgets/emoji_picker_widget.dart | 190 ++++++++++++++++-- .../features/chat/widgets/custom_input.dart | 2 + .../chat/widgets/emoji/emoji_row.dart | 1 + 3 files changed, 176 insertions(+), 17 deletions(-) diff --git a/app/lib/common/widgets/emoji_picker_widget.dart b/app/lib/common/widgets/emoji_picker_widget.dart index 8dcb63f60bfa..cc716246cb76 100644 --- a/app/lib/common/widgets/emoji_picker_widget.dart +++ b/app/lib/common/widgets/emoji_picker_widget.dart @@ -3,6 +3,8 @@ import 'dart:math'; import 'package:acter/common/themes/app_theme.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; class EmojiPickerWidget extends StatelessWidget { const EmojiPickerWidget({ @@ -10,6 +12,7 @@ class EmojiPickerWidget extends StatelessWidget { this.size, this.onEmojiSelected, this.onBackspacePressed, + required this.onClosePicker, this.withBoarder = false, }); @@ -17,6 +20,7 @@ class EmojiPickerWidget extends StatelessWidget { final bool withBoarder; final OnEmojiSelected? onEmojiSelected; final OnBackspacePressed? onBackspacePressed; + final VoidCallback onClosePicker; @override Widget build(BuildContext context) { @@ -26,6 +30,33 @@ class EmojiPickerWidget extends StatelessWidget { size == null ? MediaQuery.of(context).size.width : size!.width; final cols = min(width / (EmojiConfig.emojiSizeMax * 2), 12).floor(); + final emojiConfig = EmojiViewConfig( + backgroundColor: Theme.of(context).colorScheme.surface, + columns: cols, + emojiSizeMax: EmojiConfig.emojiSizeMax, + ); + final catConfig = CategoryViewConfig( + customCategoryView: (config, state, tab, page) => + actionBar(context, emojiConfig, state, tab, page), + ); + + final searchConfig = SearchViewConfig( + customSearchView: (_, state, showEmojiView) => _CustomSearchView( + Config( + emojiViewConfig: emojiConfig, + searchViewConfig: SearchViewConfig( + backgroundColor: Theme.of(context).colorScheme.surface, + buttonIconColor: Theme.of(context).colorScheme.onPrimary, + hintText: L10n.of(context).search, + ), + checkPlatformCompatibility: EmojiConfig.checkPlatformCompatibility, + emojiTextStyle: EmojiConfig.emojiTextStyle, + ), + state, + showEmojiView, + onClosePicker, + ), + ); return Container( padding: withBoarder ? const EdgeInsets.only(top: 10, left: 15, right: 15) @@ -54,24 +85,13 @@ class EmojiPickerWidget extends StatelessWidget { onEmojiSelected: onEmojiSelected, onBackspacePressed: onBackspacePressed, config: Config( - emojiViewConfig: EmojiViewConfig( - backgroundColor: Theme.of(context).colorScheme.surface, - columns: cols, - emojiSizeMax: EmojiConfig.emojiSizeMax, - ), - categoryViewConfig: CategoryViewConfig( - backgroundColor: Theme.of(context).colorScheme.surface, - initCategory: Category.RECENT, - ), - bottomActionBarConfig: BottomActionBarConfig( - showBackspaceButton: false, - backgroundColor: Theme.of(context).colorScheme.surface, - buttonColor: Theme.of(context).colorScheme.primary, - ), - searchViewConfig: SearchViewConfig( - backgroundColor: Theme.of(context).colorScheme.surface, - buttonIconColor: Theme.of(context).colorScheme.onPrimary, + emojiViewConfig: emojiConfig, + categoryViewConfig: catConfig, + bottomActionBarConfig: const BottomActionBarConfig( + enabled: false, ), + searchViewConfig: searchConfig, + skinToneConfig: const SkinToneConfig(), checkPlatformCompatibility: EmojiConfig.checkPlatformCompatibility, emojiTextStyle: EmojiConfig.emojiTextStyle, @@ -82,4 +102,140 @@ class EmojiPickerWidget extends StatelessWidget { ), ); } + + Widget actionBar( + BuildContext context, + EmojiViewConfig emojiConfig, + EmojiViewState state, + TabController tabController, + PageController pageController, + ) => + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: state.onShowSearchView, + icon: Icon(PhosphorIcons.magnifyingGlass()), + ), + Expanded( + child: DefaultCategoryView( + Config( + emojiViewConfig: emojiConfig, + categoryViewConfig: CategoryViewConfig( + backgroundColor: Theme.of(context).colorScheme.surface, + initCategory: Category.RECENT, + ), + ), + state, + tabController, + pageController, + ), + ), + if (onBackspacePressed != null) + IconButton( + onPressed: onBackspacePressed, + icon: Icon(PhosphorIcons.backspace()), + ), + IconButton( + onPressed: onClosePicker, + icon: Icon(PhosphorIcons.xCircle()), + ), + ], + ); +} + +/// Default Search implementation +class _CustomSearchView extends SearchView { + final VoidCallback closePicker; + + /// Constructor + const _CustomSearchView( + super.config, + super.state, + super.showEmojiView, + this.closePicker, + ); + + @override + _CustomSearchViewState createState() => _CustomSearchViewState(); +} + +/// Default Search View State +class _CustomSearchViewState extends SearchViewState<_CustomSearchView> { + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final emojiSize = + widget.config.emojiViewConfig.getEmojiSize(constraints.maxWidth); + final emojiBoxSize = + widget.config.emojiViewConfig.getEmojiBoxSize(constraints.maxWidth); + + return Container( + color: widget.config.searchViewConfig.backgroundColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Material( + color: Colors.transparent, + child: SizedBox( + height: emojiBoxSize + 8.0, + child: Row( + children: [ + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 4.0), + scrollDirection: Axis.horizontal, + itemCount: results.length, + itemBuilder: (context, index) { + return buildEmoji( + results[index], + emojiSize, + emojiBoxSize, + ); + }, + ), + ), + IconButton( + onPressed: () { + widget.closePicker(); + }, + color: widget.config.searchViewConfig.buttonIconColor, + icon: Icon(PhosphorIcons.xCircle()), + ), + ], + ), + ), + ), + Row( + children: [ + IconButton( + onPressed: () { + widget.showEmojiView(); + }, + color: widget.config.searchViewConfig.buttonIconColor, + icon: const Icon( + Icons.arrow_back, + ), + ), + Expanded( + child: TextField( + onChanged: onTextInputChanged, + focusNode: focusNode, + style: widget.config.searchViewConfig.inputTextStyle, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.config.searchViewConfig.hintText, + hintStyle: widget.config.searchViewConfig.hintTextStyle, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16), + ), + ), + ), + ], + ), + ], + ), + ); + },); + } } diff --git a/app/lib/features/chat/widgets/custom_input.dart b/app/lib/features/chat/widgets/custom_input.dart index 4046bb7fd1a4..3bec3fc9ee1d 100644 --- a/app/lib/features/chat/widgets/custom_input.dart +++ b/app/lib/features/chat/widgets/custom_input.dart @@ -422,6 +422,8 @@ class __ChatInputState extends ConsumerState<_ChatInput> { ), onEmojiSelected: handleEmojiSelected, onBackspacePressed: handleBackspacePressed, + onClosePicker: () => + ref.read(chatInputProvider.notifier).emojiPickerVisible(false), ), ], ); diff --git a/app/lib/features/chat/widgets/emoji/emoji_row.dart b/app/lib/features/chat/widgets/emoji/emoji_row.dart index 36c4a9bce485..a62f310d98d1 100644 --- a/app/lib/features/chat/widgets/emoji/emoji_row.dart +++ b/app/lib/features/chat/widgets/emoji/emoji_row.dart @@ -109,6 +109,7 @@ class EmojiRow extends StatelessWidget { onEmojiTap(message.id, emoji.emoji); Navigator.pop(context); }, + onClosePicker: () => Navigator.pop(context), ), ); } From ac4b092cdc6d0a0060a6d676c0d5cb7c2de23cc0 Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Tue, 27 Aug 2024 13:59:44 +0100 Subject: [PATCH 5/7] Add Changelog --- .changes/2109-emoji-ux.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/2109-emoji-ux.md diff --git a/.changes/2109-emoji-ux.md b/.changes/2109-emoji-ux.md new file mode 100644 index 000000000000..6bcf8d81ec1d --- /dev/null +++ b/.changes/2109-emoji-ux.md @@ -0,0 +1 @@ +- Fixes of bugs and the UX of the emoji picker: you can now close it easily with the x on the top right, even from the search. From 6dfcd74bd9b046453cdab1de1df0fe738556129c Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 28 Aug 2024 15:17:04 +0100 Subject: [PATCH 6/7] Refactor emoji search widget for simplicity --- .../common/widgets/emoji_picker_widget.dart | 141 +++++++++--------- 1 file changed, 73 insertions(+), 68 deletions(-) diff --git a/app/lib/common/widgets/emoji_picker_widget.dart b/app/lib/common/widgets/emoji_picker_widget.dart index cc716246cb76..0e723bea63ce 100644 --- a/app/lib/common/widgets/emoji_picker_widget.dart +++ b/app/lib/common/widgets/emoji_picker_widget.dart @@ -164,78 +164,83 @@ class _CustomSearchView extends SearchView { class _CustomSearchViewState extends SearchViewState<_CustomSearchView> { @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - final emojiSize = - widget.config.emojiViewConfig.getEmojiSize(constraints.maxWidth); - final emojiBoxSize = - widget.config.emojiViewConfig.getEmojiBoxSize(constraints.maxWidth); + return LayoutBuilder( + builder: (context, constraints) { + final emojiSize = + widget.config.emojiViewConfig.getEmojiSize(constraints.maxWidth); + final emojiBoxSize = + widget.config.emojiViewConfig.getEmojiBoxSize(constraints.maxWidth); - return Container( - color: widget.config.searchViewConfig.backgroundColor, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Material( - color: Colors.transparent, - child: SizedBox( - height: emojiBoxSize + 8.0, - child: Row( - children: [ - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 4.0), - scrollDirection: Axis.horizontal, - itemCount: results.length, - itemBuilder: (context, index) { - return buildEmoji( - results[index], - emojiSize, - emojiBoxSize, - ); - }, - ), - ), - IconButton( - onPressed: () { - widget.closePicker(); - }, - color: widget.config.searchViewConfig.buttonIconColor, - icon: Icon(PhosphorIcons.xCircle()), - ), - ], + return Container( + color: widget.config.searchViewConfig.backgroundColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Material( + color: Colors.transparent, + child: SizedBox( + height: emojiBoxSize + 8.0, + child: _renderResultRow(emojiSize, emojiBoxSize), ), ), + _renderSearchBox(), + ], + ), + ); + }, + ); + } + + Widget _renderResultRow(double emojiSize, double emojiBoxSize) => Row( + children: [ + Expanded( + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 4.0), + scrollDirection: Axis.horizontal, + itemCount: results.length, + itemBuilder: (context, index) { + return buildEmoji( + results[index], + emojiSize, + emojiBoxSize, + ); + }, ), - Row( - children: [ - IconButton( - onPressed: () { - widget.showEmojiView(); - }, - color: widget.config.searchViewConfig.buttonIconColor, - icon: const Icon( - Icons.arrow_back, - ), - ), - Expanded( - child: TextField( - onChanged: onTextInputChanged, - focusNode: focusNode, - style: widget.config.searchViewConfig.inputTextStyle, - decoration: InputDecoration( - border: InputBorder.none, - hintText: widget.config.searchViewConfig.hintText, - hintStyle: widget.config.searchViewConfig.hintTextStyle, - contentPadding: - const EdgeInsets.symmetric(horizontal: 16), - ), - ), - ), - ], + ), + IconButton( + onPressed: () { + widget.closePicker(); + }, + color: widget.config.searchViewConfig.buttonIconColor, + icon: Icon(PhosphorIcons.xCircle()), + ), + ], + ); + + Widget _renderSearchBox() => Row( + children: [ + IconButton( + onPressed: () { + widget.showEmojiView(); + }, + color: widget.config.searchViewConfig.buttonIconColor, + icon: const Icon( + Icons.arrow_back, ), - ], - ), + ), + Expanded( + child: TextField( + onChanged: onTextInputChanged, + focusNode: focusNode, + style: widget.config.searchViewConfig.inputTextStyle, + decoration: InputDecoration( + border: InputBorder.none, + hintText: widget.config.searchViewConfig.hintText, + hintStyle: widget.config.searchViewConfig.hintTextStyle, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + ), + ), + ], ); - },); - } } From 4c784bf9066d7fb3b4d0888d86767a4e8f73c46e Mon Sep 17 00:00:00 2001 From: Benjamin Kampmann Date: Wed, 28 Aug 2024 15:17:36 +0100 Subject: [PATCH 7/7] Fix emoji selection if there was no focus but content --- app/lib/features/chat/widgets/custom_input.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/lib/features/chat/widgets/custom_input.dart b/app/lib/features/chat/widgets/custom_input.dart index 3bec3fc9ee1d..e7c4715b348e 100644 --- a/app/lib/features/chat/widgets/custom_input.dart +++ b/app/lib/features/chat/widgets/custom_input.dart @@ -285,6 +285,8 @@ class __ChatInputState extends ConsumerState<_ChatInput> { String prefixText = ''; // Get cursor current position var cursorPos = textController.selection.base.offset; + int emojiLength = emoji.emoji.length; + if (cursorPos >= 0) { // can be -1 on empty and never accessed @@ -293,14 +295,14 @@ class __ChatInputState extends ConsumerState<_ChatInput> { // Get the left text of cursor prefixText = textController.text.substring(0, cursorPos); + textController.text = prefixText + emoji.emoji + suffixText; } else { - cursorPos = 0; + // no focus yet, add the emoji at the end of the content + cursorPos = textController.text.length; + textController.text += emoji.emoji; } - int emojiLength = emoji.emoji.length; - // Add emoji at current current cursor position - textController.text = prefixText + emoji.emoji + suffixText; // Cursor move to end of added emoji character textController.selection = TextSelection(