Skip to content

Commit

Permalink
Merge pull request #2109 from acterglobal/ben-improve-emoji-ux
Browse files Browse the repository at this point in the history
Improve the emoji UX
  • Loading branch information
gnunicorn authored Aug 28, 2024
2 parents 37cdef2 + d0468ba commit 3cc5f77
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 26 deletions.
1 change: 1 addition & 0 deletions .changes/2109-emoji-ux.md
Original file line number Diff line number Diff line change
@@ -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.
202 changes: 182 additions & 20 deletions app/lib/common/widgets/emoji_picker_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ 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({
super.key,
this.size,
this.onEmojiSelected,
this.onBackspacePressed,
required this.onClosePicker,
this.withBoarder = false,
});

final Size? size;
final bool withBoarder;
final OnEmojiSelected? onEmojiSelected;
final OnBackspacePressed? onBackspacePressed;
final VoidCallback onClosePicker;

@override
Widget build(BuildContext context) {
Expand All @@ -26,14 +30,42 @@ 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)
: 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,
Expand All @@ -53,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,
Expand All @@ -81,4 +102,145 @@ 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: _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,
);
},
),
),
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),
),
),
),
],
);
}
30 changes: 24 additions & 6 deletions app/lib/features/chat/widgets/custom_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -275,19 +275,30 @@ 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;
int emojiLength = emoji.emoji.length;

// Right text of cursor position
String suffixText = textController.text.substring(cursorPos);
if (cursorPos >= 0) {
// can be -1 on empty and never accessed

// Get the left text of cursor
String prefixText = textController.text.substring(0, cursorPos);
// Right text of cursor position
suffixText = textController.text.substring(cursorPos);

int emojiLength = emoji.emoji.length;
// Get the left text of cursor
prefixText = textController.text.substring(0, cursorPos);
textController.text = prefixText + emoji.emoji + suffixText;
} else {
// no focus yet, add the emoji at the end of the content
cursorPos = textController.text.length;
textController.text += emoji.emoji;
}

// Add emoji at current current cursor position
textController.text = prefixText + emoji.emoji + suffixText;

// Cursor move to end of added emoji character
textController.selection = TextSelection(
Expand All @@ -305,6 +316,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;
}
Expand Down Expand Up @@ -404,6 +420,8 @@ class __ChatInputState extends ConsumerState<_ChatInput> {
),
onEmojiSelected: handleEmojiSelected,
onBackspacePressed: handleBackspacePressed,
onClosePicker: () =>
ref.read(chatInputProvider.notifier).emojiPickerVisible(false),
),
],
);
Expand Down
1 change: 1 addition & 0 deletions app/lib/features/chat/widgets/emoji/emoji_row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class EmojiRow extends StatelessWidget {
onEmojiTap(message.id, emoji.emoji);
Navigator.pop(context);
},
onClosePicker: () => Navigator.pop(context),
),
);
}

0 comments on commit 3cc5f77

Please sign in to comment.