diff --git a/frontend/app_flowy/assets/images/editor/insert_emoticon.svg b/frontend/app_flowy/assets/images/editor/insert_emoticon.svg
new file mode 100644
index 0000000000..8bb960e52d
--- /dev/null
+++ b/frontend/app_flowy/assets/images/editor/insert_emoticon.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/frontend/app_flowy/assets/images/editor/insert_emoticon_2.svg b/frontend/app_flowy/assets/images/editor/insert_emoticon_2.svg
new file mode 100644
index 0000000000..66bbf7a626
--- /dev/null
+++ b/frontend/app_flowy/assets/images/editor/insert_emoticon_2.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/tool_bar.dart b/frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/tool_bar.dart
index 54d6c12ea1..faf49e8027 100644
--- a/frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/tool_bar.dart
+++ b/frontend/app_flowy/lib/workspace/presentation/stack_page/doc/widget/toolbar/tool_bar.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math';
import 'package:app_flowy/workspace/presentation/stack_page/doc/widget/toolbar/history_button.dart';
+import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter/material.dart';
@@ -151,6 +152,11 @@ class EditorToolbar extends StatelessWidget implements PreferredSizeWidget {
controller: controller,
iconSize: toolbarIconSize,
),
+ FlowyEmojiStyleButton(
+ normalIcon: 'editor/insert_emoticon',
+ controller: controller,
+ tooltipText: "Emoji Picker",
+ ),
],
);
}
diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/emoji_picker.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/emoji_picker.dart
new file mode 100644
index 0000000000..f2cc8bb440
--- /dev/null
+++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/emoji_picker.dart
@@ -0,0 +1,5 @@
+export 'src/config.dart';
+export 'src/models/emoji_model.dart';
+export 'src/emoji_picker.dart';
+export 'src/emoji_picker_builder.dart';
+export 'src/emoji_button.dart';
diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/config.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/config.dart
new file mode 100644
index 0000000000..b7e71e87b9
--- /dev/null
+++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/config.dart
@@ -0,0 +1,164 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+
+import 'models/category_models.dart';
+import 'emoji_picker.dart';
+
+/// Config for customizations
+class Config {
+ /// Constructor
+ const Config(
+ {this.columns = 7,
+ this.emojiSizeMax = 32.0,
+ this.verticalSpacing = 0,
+ this.horizontalSpacing = 0,
+ this.initCategory = Category.RECENT,
+ this.bgColor = const Color(0xFFEBEFF2),
+ this.indicatorColor = Colors.blue,
+ this.iconColor = Colors.grey,
+ this.iconColorSelected = Colors.blue,
+ this.progressIndicatorColor = Colors.blue,
+ this.backspaceColor = Colors.blue,
+ this.showRecentsTab = true,
+ this.recentsLimit = 28,
+ this.noRecentsText = 'No Recents',
+ this.noRecentsStyle = const TextStyle(fontSize: 20, color: Colors.black26),
+ this.tabIndicatorAnimDuration = kTabScrollDuration,
+ this.categoryIcons = const CategoryIcons(),
+ this.buttonMode = ButtonMode.MATERIAL});
+
+ /// Number of emojis per row
+ final int columns;
+
+ /// Width and height the emoji will be maximal displayed
+ /// Can be smaller due to screen size and amount of columns
+ final double emojiSizeMax;
+
+ /// Verical spacing between emojis
+ final double verticalSpacing;
+
+ /// Horizontal spacing between emojis
+ final double horizontalSpacing;
+
+ /// The initial [Category] that will be selected
+ /// This [Category] will have its button in the bottombar darkened
+ final Category initCategory;
+
+ /// The background color of the Widget
+ final Color bgColor;
+
+ /// The color of the category indicator
+ final Color indicatorColor;
+
+ /// The color of the category icons
+ final Color iconColor;
+
+ /// The color of the category icon when selected
+ final Color iconColorSelected;
+
+ /// The color of the loading indicator during initalization
+ final Color progressIndicatorColor;
+
+ /// The color of the backspace icon button
+ final Color backspaceColor;
+
+ /// Show extra tab with recently used emoji
+ final bool showRecentsTab;
+
+ /// Limit of recently used emoji that will be saved
+ final int recentsLimit;
+
+ /// The text to be displayed if no recent emojis to display
+ final String noRecentsText;
+
+ /// The text style for [noRecentsText]
+ final TextStyle noRecentsStyle;
+
+ /// Duration of tab indicator to animate to next category
+ final Duration tabIndicatorAnimDuration;
+
+ /// Determines the icon to display for each [Category]
+ final CategoryIcons categoryIcons;
+
+ /// Change between Material and Cupertino button style
+ final ButtonMode buttonMode;
+
+ /// Get Emoji size based on properties and screen width
+ double getEmojiSize(double width) {
+ final maxSize = width / columns;
+ return min(maxSize, emojiSizeMax);
+ }
+
+ /// Returns the icon for the category
+ IconData getIconForCategory(Category category) {
+ switch (category) {
+ case Category.RECENT:
+ return categoryIcons.recentIcon;
+ case Category.SMILEYS:
+ return categoryIcons.smileyIcon;
+ case Category.ANIMALS:
+ return categoryIcons.animalIcon;
+ case Category.FOODS:
+ return categoryIcons.foodIcon;
+ case Category.TRAVEL:
+ return categoryIcons.travelIcon;
+ case Category.ACTIVITIES:
+ return categoryIcons.activityIcon;
+ case Category.OBJECTS:
+ return categoryIcons.objectIcon;
+ case Category.SYMBOLS:
+ return categoryIcons.symbolIcon;
+ case Category.FLAGS:
+ return categoryIcons.flagIcon;
+ case Category.SEARCH:
+ return categoryIcons.searchIcon;
+ default:
+ throw Exception('Unsupported Category');
+ }
+ }
+
+ @override
+ bool operator ==(other) {
+ return (other is Config) &&
+ other.columns == columns &&
+ other.emojiSizeMax == emojiSizeMax &&
+ other.verticalSpacing == verticalSpacing &&
+ other.horizontalSpacing == horizontalSpacing &&
+ other.initCategory == initCategory &&
+ other.bgColor == bgColor &&
+ other.indicatorColor == indicatorColor &&
+ other.iconColor == iconColor &&
+ other.iconColorSelected == iconColorSelected &&
+ other.progressIndicatorColor == progressIndicatorColor &&
+ other.backspaceColor == backspaceColor &&
+ other.showRecentsTab == showRecentsTab &&
+ other.recentsLimit == recentsLimit &&
+ other.noRecentsText == noRecentsText &&
+ other.noRecentsStyle == noRecentsStyle &&
+ other.tabIndicatorAnimDuration == tabIndicatorAnimDuration &&
+ other.categoryIcons == categoryIcons &&
+ other.buttonMode == buttonMode;
+ }
+
+ @override
+ int get hashCode =>
+ columns.hashCode ^
+ emojiSizeMax.hashCode ^
+ verticalSpacing.hashCode ^
+ horizontalSpacing.hashCode ^
+ initCategory.hashCode ^
+ bgColor.hashCode ^
+ indicatorColor.hashCode ^
+ iconColor.hashCode ^
+ iconColorSelected.hashCode ^
+ progressIndicatorColor.hashCode ^
+ backspaceColor.hashCode ^
+ showRecentsTab.hashCode ^
+ recentsLimit.hashCode ^
+ noRecentsText.hashCode ^
+ noRecentsStyle.hashCode ^
+ tabIndicatorAnimDuration.hashCode ^
+ categoryIcons.hashCode ^
+ buttonMode.hashCode;
+}
diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart
new file mode 100644
index 0000000000..afdfa5e6eb
--- /dev/null
+++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/default_emoji_picker_view.dart
@@ -0,0 +1,276 @@
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+import 'models/category_models.dart';
+import 'config.dart';
+import 'models/emoji_model.dart';
+import 'emoji_picker.dart';
+import 'emoji_picker_builder.dart';
+import 'emoji_view_state.dart';
+
+class DefaultEmojiPickerView extends EmojiPickerBuilder {
+ const DefaultEmojiPickerView(Config config, EmojiViewState state, {Key? key}) : super(config, state, key: key);
+
+ @override
+ _DefaultEmojiPickerViewState createState() => _DefaultEmojiPickerViewState();
+}
+
+class _DefaultEmojiPickerViewState extends State with TickerProviderStateMixin {
+ PageController? _pageController;
+ TabController? _tabController;
+ final TextEditingController _emojiController = TextEditingController();
+ final FocusNode _emojiFocusNode = FocusNode();
+ final CategoryEmoji _categoryEmoji = CategoryEmoji(Category.SEARCH, List.empty(growable: true));
+ CategoryEmoji searchEmojiList = CategoryEmoji(Category.SEARCH, []);
+
+ @override
+ void initState() {
+ var initCategory =
+ widget.state.categoryEmoji.indexWhere((element) => element.category == widget.config.initCategory);
+ if (initCategory == -1) {
+ initCategory = 0;
+ }
+ _tabController = TabController(initialIndex: initCategory, length: widget.state.categoryEmoji.length, vsync: this);
+ _pageController = PageController(initialPage: initCategory);
+ _emojiFocusNode.requestFocus();
+
+ _emojiController.addListener(() {
+ String query = _emojiController.text.toLowerCase();
+ if (query.isEmpty) {
+ searchEmojiList.emoji.clear();
+ _pageController!.jumpToPage(
+ _tabController!.index,
+ );
+ } else {
+ searchEmojiList.emoji.clear();
+ for (var element in widget.state.categoryEmoji) {
+ searchEmojiList.emoji.addAll(
+ element.emoji.where((item) {
+ return item.name.toLowerCase().contains(query);
+ }).toList(),
+ );
+ }
+ }
+ setState(() {});
+ });
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ _emojiController.dispose();
+ _emojiFocusNode.dispose();
+ super.dispose();
+ }
+
+ Widget _buildBackspaceButton() {
+ if (widget.state.onBackspacePressed != null) {
+ return Material(
+ type: MaterialType.transparency,
+ child: IconButton(
+ padding: const EdgeInsets.only(bottom: 2),
+ icon: Icon(
+ Icons.backspace,
+ color: widget.config.backspaceColor,
+ ),
+ onPressed: () {
+ widget.state.onBackspacePressed!();
+ }),
+ );
+ }
+ return Container();
+ }
+
+ bool isEmojiSearching() {
+ bool result = searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty;
+
+ return result;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ final emojiSize = widget.config.getEmojiSize(constraints.maxWidth);
+
+ return Container(
+ color: widget.config.bgColor,
+ padding: const EdgeInsets.all(5.0),
+ child: Column(
+ children: [
+ SizedBox(
+ height: 25.0,
+ child: TextField(
+ controller: _emojiController,
+ focusNode: _emojiFocusNode,
+ autofocus: true,
+ style: const TextStyle(fontSize: 14.0),
+ cursorWidth: 1.0,
+ cursorColor: Colors.black,
+ decoration: InputDecoration(
+ contentPadding: const EdgeInsets.symmetric(horizontal: 5.0),
+ hintText: "Search emoji",
+ focusedBorder: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4.0),
+ borderSide: const BorderSide(),
+ gapPadding: 0.0,
+ ),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(4.0),
+ borderSide: const BorderSide(),
+ gapPadding: 0.0,
+ ),
+ filled: true,
+ fillColor: Colors.white,
+ hoverColor: Colors.white,
+ ),
+ ),
+ ),
+ Row(
+ children: [
+ Expanded(
+ child: TabBar(
+ labelColor: widget.config.iconColorSelected,
+ unselectedLabelColor: widget.config.iconColor,
+ controller: isEmojiSearching() ? TabController(length: 1, vsync: this) : _tabController,
+ labelPadding: EdgeInsets.zero,
+ indicatorColor: widget.config.indicatorColor,
+ padding: const EdgeInsets.symmetric(vertical: 5.0),
+ indicator: BoxDecoration(
+ border: Border.all(color: Colors.transparent),
+ borderRadius: BorderRadius.circular(4.0),
+ color: Colors.grey.withOpacity(0.5),
+ ),
+ onTap: (index) {
+ _pageController!.animateToPage(
+ index,
+ duration: widget.config.tabIndicatorAnimDuration,
+ curve: Curves.ease,
+ );
+ },
+ tabs: isEmojiSearching()
+ ? [_buildCategory(Category.SEARCH, emojiSize)]
+ : widget.state.categoryEmoji
+ .asMap()
+ .entries
+ .map((item) => _buildCategory(item.value.category, emojiSize))
+ .toList(),
+ ),
+ ),
+ _buildBackspaceButton(),
+ ],
+ ),
+ Flexible(
+ child: PageView.builder(
+ itemCount: searchEmojiList.emoji.isNotEmpty ? 1 : widget.state.categoryEmoji.length,
+ controller: _pageController,
+ physics: const NeverScrollableScrollPhysics(),
+ // onPageChanged: (index) {
+ // _tabController!.animateTo(
+ // index,
+ // duration: widget.config.tabIndicatorAnimDuration,
+ // );
+ // },
+ itemBuilder: (context, index) {
+ CategoryEmoji catEmoji = isEmojiSearching() ? searchEmojiList : widget.state.categoryEmoji[index];
+ return _buildPage(emojiSize, catEmoji);
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildCategory(Category category, double categorySize) {
+ return Tab(
+ height: categorySize,
+ child: Icon(
+ widget.config.getIconForCategory(category),
+ size: categorySize / 1.3,
+ ),
+ );
+ }
+
+ Widget _buildButtonWidget({required VoidCallback onPressed, required Widget child}) {
+ if (widget.config.buttonMode == ButtonMode.MATERIAL) {
+ return TextButton(
+ onPressed: onPressed,
+ child: child,
+ style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.zero)),
+ );
+ }
+ return CupertinoButton(padding: EdgeInsets.zero, onPressed: onPressed, child: child);
+ }
+
+ Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) {
+ // Display notice if recent has no entries yet
+ final scrollController = ScrollController();
+
+ if (categoryEmoji.category == Category.RECENT && categoryEmoji.emoji.isEmpty) {
+ return _buildNoRecent();
+ } else if (categoryEmoji.category == Category.SEARCH && categoryEmoji.emoji.isEmpty) {
+ return const Center(child: Text("No Emoji Found"));
+ }
+ // Build page normally
+ return ScrollbarListStack(
+ axis: Axis.vertical,
+ controller: scrollController,
+ barSize: 4.0,
+ scrollbarPadding: const EdgeInsets.symmetric(horizontal: 5.0),
+ handleColor: const Color(0xffDFE0E0),
+ trackColor: const Color(0xffDFE0E0),
+ child: ScrollConfiguration(
+ behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
+ child: GridView.count(
+ scrollDirection: Axis.vertical,
+ physics: const ScrollPhysics(),
+ controller: scrollController,
+ shrinkWrap: true,
+ // primary: true,
+ padding: const EdgeInsets.all(0),
+ crossAxisCount: widget.config.columns,
+ mainAxisSpacing: widget.config.verticalSpacing,
+ crossAxisSpacing: widget.config.horizontalSpacing,
+ children: _categoryEmoji.emoji.isNotEmpty
+ ? _categoryEmoji.emoji.map((e) => _buildEmoji(emojiSize, categoryEmoji, e)).toList()
+ : categoryEmoji.emoji.map((item) => _buildEmoji(emojiSize, categoryEmoji, item)).toList(),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildEmoji(
+ double emojiSize,
+ CategoryEmoji categoryEmoji,
+ Emoji emoji,
+ ) {
+ return _buildButtonWidget(
+ onPressed: () {
+ widget.state.onEmojiSelected(categoryEmoji.category, emoji);
+ },
+ child: FittedBox(
+ fit: BoxFit.fill,
+ child: Text(
+ emoji.emoji,
+ textScaleFactor: 1.0,
+ style: TextStyle(
+ fontSize: emojiSize,
+ backgroundColor: Colors.transparent,
+ ),
+ ),
+ ));
+ }
+
+ Widget _buildNoRecent() {
+ return Center(
+ child: Text(
+ widget.config.noRecentsText,
+ style: widget.config.noRecentsStyle,
+ textAlign: TextAlign.center,
+ ));
+ }
+}
diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart
new file mode 100644
index 0000000000..4fdc3bd14f
--- /dev/null
+++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart
@@ -0,0 +1,174 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_quill/flutter_quill.dart';
+
+import 'package:app_flowy/workspace/presentation/stack_page/doc/widget/toolbar/toolbar_icon_button.dart';
+import 'package:app_flowy/workspace/presentation/widgets/emoji_picker/emoji_picker.dart';
+
+class FlowyEmojiStyleButton extends StatefulWidget {
+ // final Attribute attribute;
+ final String normalIcon;
+ final double iconSize;
+ final QuillController controller;
+ final String tooltipText;
+
+ const FlowyEmojiStyleButton({
+ // required this.attribute,
+ required this.normalIcon,
+ required this.controller,
+ required this.tooltipText,
+ this.iconSize = defaultIconSize,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ _EmojiStyleButtonState createState() => _EmojiStyleButtonState();
+}
+
+class _EmojiStyleButtonState extends State {
+ bool _isToggled = false;
+ // Style get _selectionStyle => widget.controller.getSelectionStyle();
+ final GlobalKey emojiButtonKey = GlobalKey();
+ OverlayEntry _entry = OverlayEntry(builder: (context) => Container());
+ // final FocusNode _keyFocusNode = FocusNode();
+
+ @override
+ void initState() {
+ super.initState();
+ // _isToggled = _getIsToggled(_selectionStyle.attributes);
+ // widget.controller.addListener(_didChangeEditingValue);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // debugPrint(MediaQuery.of(context).size.width.toString());
+ // debugPrint(MediaQuery.of(context).size.height.toString());
+
+ return ToolbarIconButton(
+ key: emojiButtonKey,
+ onPressed: _toggleAttribute,
+ width: widget.iconSize * kIconButtonFactor,
+ isToggled: _isToggled,
+ iconName: widget.normalIcon,
+ tooltipText: widget.tooltipText,
+ );
+ }
+
+ // @override
+ // void didUpdateWidget(covariant FlowyEmojiStyleButton oldWidget) {
+ // super.didUpdateWidget(oldWidget);
+ // if (oldWidget.controller != widget.controller) {
+ // oldWidget.controller.removeListener(_didChangeEditingValue);
+ // widget.controller.addListener(_didChangeEditingValue);
+ // _isToggled = _getIsToggled(_selectionStyle.attributes);
+ // }
+ // }
+
+ // @override
+ // void dispose() {
+ // widget.controller.removeListener(_didChangeEditingValue);
+ // super.dispose();
+ // }
+
+ // void _didChangeEditingValue() {
+ // setState(() => _isToggled = _getIsToggled(_selectionStyle.attributes));
+ // }
+
+ // bool _getIsToggled(Map attrs) {
+ // return _entry.mounted;
+ // }
+
+ void _toggleAttribute() {
+ if (_entry.mounted) {
+ _entry.remove();
+ setState(() => _isToggled = false);
+ } else {
+ RenderBox box = emojiButtonKey.currentContext?.findRenderObject() as RenderBox;
+ Offset position = box.localToGlobal(Offset.zero);
+
+ // final window = await getWindowInfo();
+
+ _entry = OverlayEntry(
+ builder: (BuildContext context) => BuildEmojiPickerView(
+ controller: widget.controller,
+ offset: position,
+ ),
+ );
+
+ Overlay.of(context)!.insert(_entry);
+ setState(() => _isToggled = true);
+ }
+
+ //TODO @gaganyadav80: INFO: throws error when using TextField with FlowyOverlay.
+
+ // FlowyOverlay.of(context).insertWithRect(
+ // widget: BuildEmojiPickerView(controller: widget.controller),
+ // identifier: 'overlay_emoji_picker',
+ // anchorPosition: Offset(position.dx + 40, position.dy - 10),
+ // anchorSize: window.frame.size,
+ // anchorDirection: AnchorDirection.topLeft,
+ // style: FlowyOverlayStyle(blur: true),
+ // );
+ }
+}
+
+class BuildEmojiPickerView extends StatefulWidget {
+ const BuildEmojiPickerView({Key? key, required this.controller, this.offset}) : super(key: key);
+
+ final QuillController controller;
+ final Offset? offset;
+
+ @override
+ State createState() => _BuildEmojiPickerViewState();
+}
+
+class _BuildEmojiPickerViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Stack(
+ children: [
+ Positioned(
+ //TODO @gaganyadav80: Not sure about the calculated position.
+ top: widget.offset!.dy - MediaQuery.of(context).size.height / 2.83 - 30,
+ left: widget.offset!.dx - MediaQuery.of(context).size.width / 3.92 + 40,
+ child: Material(
+ borderRadius: BorderRadius.circular(8.0),
+ child: SizedBox(
+ //TODO @gaganyadav80: FIXIT: Gets too large when fullscreen.
+ height: MediaQuery.of(context).size.height / 2.83 + 20,
+ width: MediaQuery.of(context).size.width / 3.92,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(8.0),
+ child: EmojiPicker(
+ onEmojiSelected: (category, emoji) => insertEmoji(emoji),
+ config: const Config(
+ columns: 8,
+ emojiSizeMax: 28,
+ bgColor: Color(0xffF2F2F2),
+ iconColor: Colors.grey,
+ iconColorSelected: Color(0xff333333),
+ indicatorColor: Color(0xff333333),
+ progressIndicatorColor: Color(0xff333333),
+ buttonMode: ButtonMode.CUPERTINO,
+ initCategory: Category.RECENT,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ void insertEmoji(Emoji emoji) {
+ final baseOffset = widget.controller.selection.baseOffset;
+ final extentOffset = widget.controller.selection.extentOffset;
+ final replaceLen = extentOffset - baseOffset;
+ final selection = widget.controller.selection.copyWith(
+ baseOffset: baseOffset + emoji.emoji.length,
+ extentOffset: baseOffset + emoji.emoji.length,
+ );
+
+ widget.controller.replaceText(baseOffset, replaceLen, emoji.emoji, selection);
+ }
+}
diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart
new file mode 100644
index 0000000000..8bb45b51cd
--- /dev/null
+++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart
@@ -0,0 +1,3223 @@
+// Copyright information
+// File originally from https://github.com/JeffG05/emoji_picker
+
+// import 'emoji.dart';
+
+// final List