mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-27 06:39:25 +01:00
feat: Emoji related fixes
- adds Emoji autocomplete following popular `:` hotkey - adds Famedly's famous smart Emojis (tm) - syncs recent Emojis with SDK Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
parent
3e3858d729
commit
3e80e3f67e
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/utils/fluffy_emoji_picker.dart';
|
||||||
import 'chat.dart';
|
import 'chat.dart';
|
||||||
|
|
||||||
class ChatEmojiPicker extends StatelessWidget {
|
class ChatEmojiPicker extends StatelessWidget {
|
||||||
@ -19,6 +20,7 @@ class ChatEmojiPicker extends StatelessWidget {
|
|||||||
? EmojiPicker(
|
? EmojiPicker(
|
||||||
onEmojiSelected: controller.onEmojiSelected,
|
onEmojiSelected: controller.onEmojiSelected,
|
||||||
onBackspacePressed: controller.emojiPickerBackspace,
|
onBackspacePressed: controller.emojiPickerBackspace,
|
||||||
|
customWidget: (c, s) => FluffyEmojiPickerView(c, s),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:emojis/emoji.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
@ -54,7 +55,7 @@ class InputBar extends StatelessWidget {
|
|||||||
final List<Map<String, String?>> ret = <Map<String, String?>>[];
|
final List<Map<String, String?>> ret = <Map<String, String?>>[];
|
||||||
const maxResults = 30;
|
const maxResults = 30;
|
||||||
|
|
||||||
final commandMatch = RegExp(r'^\/([\w]*)$').firstMatch(searchText);
|
final commandMatch = RegExp(r'^/(\w*)$').firstMatch(searchText);
|
||||||
if (commandMatch != null) {
|
if (commandMatch != null) {
|
||||||
final commandSearch = commandMatch[1]!.toLowerCase();
|
final commandSearch = commandMatch[1]!.toLowerCase();
|
||||||
for (final command in room.client.commands.keys) {
|
for (final command in room.client.commands.keys) {
|
||||||
@ -114,6 +115,39 @@ class InputBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// aside of emote packs, also propose normal (tm) unicode emojis
|
||||||
|
final matchingUnicodeEmojis = Emoji.all()
|
||||||
|
.where((element) => [element.name, ...element.keywords]
|
||||||
|
.any((element) => element.toLowerCase().contains(emoteSearch)))
|
||||||
|
.toList();
|
||||||
|
// sort by the index of the search term in the name in order to have
|
||||||
|
// best matches first
|
||||||
|
// (thanks for the hint by github.com/nextcloud/circles devs)
|
||||||
|
matchingUnicodeEmojis.sort((a, b) {
|
||||||
|
final indexA = a.name.indexOf(emoteSearch);
|
||||||
|
final indexB = b.name.indexOf(emoteSearch);
|
||||||
|
if (indexA == -1 || indexB == -1) {
|
||||||
|
if (indexA == indexB) return 0;
|
||||||
|
if (indexA == -1) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indexA.compareTo(indexB);
|
||||||
|
});
|
||||||
|
for (final emoji in matchingUnicodeEmojis) {
|
||||||
|
ret.add({
|
||||||
|
'type': 'emoji',
|
||||||
|
'emoji': emoji.char,
|
||||||
|
// don't include sub-group names, splitting at `:` hence
|
||||||
|
'label': '${emoji.char} - ${emoji.name.split(':').first}',
|
||||||
|
'current_word': ':$emoteSearch',
|
||||||
|
});
|
||||||
|
if (ret.length > maxResults) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final userMatch = RegExp(r'(?:\s|^)@([-\w]+)$').firstMatch(searchText);
|
final userMatch = RegExp(r'(?:\s|^)@([-\w]+)$').firstMatch(searchText);
|
||||||
if (userMatch != null) {
|
if (userMatch != null) {
|
||||||
@ -205,6 +239,17 @@ class InputBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (suggestion['type'] == 'emoji') {
|
||||||
|
final label = suggestion['label']!;
|
||||||
|
return Tooltip(
|
||||||
|
message: label,
|
||||||
|
waitDuration: const Duration(days: 1), // don't show on hover
|
||||||
|
child: Container(
|
||||||
|
padding: padding,
|
||||||
|
child: Text(label, style: const TextStyle(fontFamily: 'monospace')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
if (suggestion['type'] == 'emote') {
|
if (suggestion['type'] == 'emote') {
|
||||||
final ratio = MediaQuery.of(context).devicePixelRatio;
|
final ratio = MediaQuery.of(context).devicePixelRatio;
|
||||||
final url = Uri.parse(suggestion['mxc'] ?? '').getThumbnail(
|
final url = Uri.parse(suggestion['mxc'] ?? '').getThumbnail(
|
||||||
@ -282,10 +327,17 @@ class InputBar extends StatelessWidget {
|
|||||||
if (suggestion['type'] == 'command') {
|
if (suggestion['type'] == 'command') {
|
||||||
insertText = suggestion['name']! + ' ';
|
insertText = suggestion['name']! + ' ';
|
||||||
startText = replaceText.replaceAllMapped(
|
startText = replaceText.replaceAllMapped(
|
||||||
RegExp(r'^(\/[\w]*)$'),
|
RegExp(r'^(/\w*)$'),
|
||||||
(Match m) => '/' + insertText,
|
(Match m) => '/' + insertText,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (suggestion['type'] == 'emoji') {
|
||||||
|
insertText = suggestion['emoji']! + ' ';
|
||||||
|
startText = replaceText.replaceAllMapped(
|
||||||
|
suggestion['current_word']!,
|
||||||
|
(Match m) => insertText,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (suggestion['type'] == 'emote') {
|
if (suggestion['type'] == 'emote') {
|
||||||
var isUnique = true;
|
var isUnique = true;
|
||||||
final insertEmote = suggestion['name'];
|
final insertEmote = suggestion['name'];
|
||||||
@ -376,10 +428,8 @@ class InputBar extends StatelessWidget {
|
|||||||
hideOnEmpty: true,
|
hideOnEmpty: true,
|
||||||
hideOnLoading: true,
|
hideOnLoading: true,
|
||||||
keepSuggestionsOnSuggestionSelected: true,
|
keepSuggestionsOnSuggestionSelected: true,
|
||||||
|
debounceDuration: const Duration(milliseconds: 50),
|
||||||
debounceDuration: const Duration(
|
// show suggestions after 50ms idle time (default is 300)
|
||||||
milliseconds:
|
|
||||||
50), // show suggestions after 50ms idle time (default is 300)
|
|
||||||
textFieldConfiguration: TextFieldConfiguration(
|
textFieldConfiguration: TextFieldConfiguration(
|
||||||
minLines: minLines,
|
minLines: minLines,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
@ -407,8 +457,8 @@ class InputBar extends StatelessWidget {
|
|||||||
onSuggestionSelected: (Map<String, String?> suggestion) =>
|
onSuggestionSelected: (Map<String, String?> suggestion) =>
|
||||||
insertSuggestion(context, suggestion),
|
insertSuggestion(context, suggestion),
|
||||||
errorBuilder: (BuildContext context, Object? error) => Container(),
|
errorBuilder: (BuildContext context, Object? error) => Container(),
|
||||||
loadingBuilder: (BuildContext context) =>
|
loadingBuilder: (BuildContext context) => Container(),
|
||||||
Container(), // fix loading briefly flickering a dark box
|
// fix loading briefly flickering a dark box
|
||||||
noItemsFoundBuilder: (BuildContext context) =>
|
noItemsFoundBuilder: (BuildContext context) =>
|
||||||
Container(), // fix loading briefly showing no suggestions
|
Container(), // fix loading briefly showing no suggestions
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:emoji_proposal/emoji_proposal.dart';
|
||||||
import 'package:matrix/matrix.dart';
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/config/app_config.dart';
|
import 'package:fluffychat/config/app_config.dart';
|
||||||
@ -8,6 +9,7 @@ import 'package:fluffychat/pages/chat/chat.dart';
|
|||||||
|
|
||||||
class ReactionsPicker extends StatelessWidget {
|
class ReactionsPicker extends StatelessWidget {
|
||||||
final ChatController controller;
|
final ChatController controller;
|
||||||
|
|
||||||
const ReactionsPicker(this.controller, {Key? key}) : super(key: key);
|
const ReactionsPicker(this.controller, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -26,7 +28,13 @@ class ReactionsPicker extends StatelessWidget {
|
|||||||
if (!display) {
|
if (!display) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
final emojis = List<String>.from(AppEmojis.emojis);
|
final proposals = proposeEmojis(
|
||||||
|
controller.selectedEvents.first.plaintextBody,
|
||||||
|
number: 25,
|
||||||
|
languageCodes: EmojiProposalLanguageCodes.values.toSet());
|
||||||
|
final emojis = proposals.isNotEmpty
|
||||||
|
? proposals.map((e) => e.char).toList()
|
||||||
|
: List<String>.from(AppEmojis.emojis);
|
||||||
final allReactionEvents = controller.selectedEvents.first
|
final allReactionEvents = controller.selectedEvents.first
|
||||||
.aggregatedEvents(
|
.aggregatedEvents(
|
||||||
controller.timeline!, RelationshipTypes.reaction)
|
controller.timeline!, RelationshipTypes.reaction)
|
||||||
|
398
lib/utils/fluffy_emoji_picker.dart
Normal file
398
lib/utils/fluffy_emoji_picker.dart
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
// ignore_for_file: prefer_function_declarations_over_variables, implementation_imports
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/category_emoji.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_picker_internal_utils.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_skin_tones.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_view_state.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/triangle_shape.dart';
|
||||||
|
import 'package:emojis/emoji.dart' as emoji;
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
|
/// Default EmojiPicker Implementation - adjusted for FluffyChat
|
||||||
|
///
|
||||||
|
/// Copied and adjusted from: [DefaultEmojiPickerView]
|
||||||
|
class FluffyEmojiPickerView extends EmojiPickerBuilder {
|
||||||
|
/// Constructor
|
||||||
|
FluffyEmojiPickerView(Config config, EmojiViewState state)
|
||||||
|
: super(config, state);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_DefaultEmojiPickerViewState createState() => _DefaultEmojiPickerViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DefaultEmojiPickerViewState extends State<FluffyEmojiPickerView>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
PageController? _pageController;
|
||||||
|
TabController? _tabController;
|
||||||
|
OverlayEntry? _overlay;
|
||||||
|
late final _scrollController = ScrollController();
|
||||||
|
late final _utils = EmojiPickerInternalUtils();
|
||||||
|
final int _skinToneCount = 6;
|
||||||
|
final double tabBarHeight = 46;
|
||||||
|
|
||||||
|
@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)
|
||||||
|
..addListener(_closeSkinToneDialog);
|
||||||
|
_scrollController.addListener(_closeSkinToneDialog);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_closeSkinToneDialog();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _closeSkinToneDialog() {
|
||||||
|
_overlay?.remove();
|
||||||
|
_overlay = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openSkinToneDialog(
|
||||||
|
Emoji emoji,
|
||||||
|
double emojiSize,
|
||||||
|
CategoryEmoji categoryEmoji,
|
||||||
|
int index,
|
||||||
|
) {
|
||||||
|
_overlay = _buildSkinToneOverlay(
|
||||||
|
emoji,
|
||||||
|
emojiSize,
|
||||||
|
categoryEmoji,
|
||||||
|
index,
|
||||||
|
);
|
||||||
|
Overlay.of(context)?.insert(_overlay!);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final emojiSize = widget.config.getEmojiSize(constraints.maxWidth);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: widget.config.bgColor,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
height: tabBarHeight,
|
||||||
|
child: TabBar(
|
||||||
|
labelColor: widget.config.iconColorSelected,
|
||||||
|
indicatorColor: widget.config.indicatorColor,
|
||||||
|
unselectedLabelColor: widget.config.iconColor,
|
||||||
|
controller: _tabController,
|
||||||
|
labelPadding: EdgeInsets.zero,
|
||||||
|
onTap: (index) {
|
||||||
|
_closeSkinToneDialog();
|
||||||
|
_pageController!.jumpToPage(index);
|
||||||
|
},
|
||||||
|
tabs: widget.state.categoryEmoji
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map<Widget>((item) =>
|
||||||
|
_buildCategory(item.key, item.value.category))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildBackspaceButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: PageView.builder(
|
||||||
|
itemCount: widget.state.categoryEmoji.length,
|
||||||
|
controller: _pageController,
|
||||||
|
onPageChanged: (index) {
|
||||||
|
_tabController!.animateTo(
|
||||||
|
index,
|
||||||
|
duration: widget.config.tabIndicatorAnimDuration,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) =>
|
||||||
|
_buildPage(emojiSize, widget.state.categoryEmoji[index]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCategory(int index, Category category) {
|
||||||
|
return Tab(
|
||||||
|
icon: Icon(
|
||||||
|
widget.config.getIconForCategory(category),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPage(double emojiSize, CategoryEmoji categoryEmoji) {
|
||||||
|
final matrix = Matrix.of(context);
|
||||||
|
|
||||||
|
// Display notice if recent has no entries yet
|
||||||
|
if (categoryEmoji.category == Category.RECENT) {
|
||||||
|
final recent = matrix.client.recentEmojis;
|
||||||
|
final sorted = recent.keys.toList()
|
||||||
|
..sort((a, b) => recent[b]!.compareTo(recent[a]!));
|
||||||
|
categoryEmoji.emoji = sorted
|
||||||
|
.map((char) => Emoji(emoji.Emoji.byChar(char)?.name ?? '', char))
|
||||||
|
.toList();
|
||||||
|
if (categoryEmoji.emoji.isEmpty) {
|
||||||
|
return _buildNoRecent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Build page normally
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _closeSkinToneDialog,
|
||||||
|
child: GridView.count(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
physics: const ScrollPhysics(),
|
||||||
|
controller: _scrollController,
|
||||||
|
shrinkWrap: true,
|
||||||
|
primary: false,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
crossAxisCount: widget.config.columns,
|
||||||
|
mainAxisSpacing: widget.config.verticalSpacing,
|
||||||
|
crossAxisSpacing: widget.config.horizontalSpacing,
|
||||||
|
children: categoryEmoji.emoji.asMap().entries.map((item) {
|
||||||
|
final index = item.key;
|
||||||
|
final emoji = item.value;
|
||||||
|
final onPressed = () {
|
||||||
|
_closeSkinToneDialog();
|
||||||
|
matrix.client.addRecentEmoji(emoji.emoji);
|
||||||
|
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
|
||||||
|
};
|
||||||
|
|
||||||
|
final onLongPressed = () {
|
||||||
|
if (!emoji.hasSkinTone || !widget.config.enableSkinTones) {
|
||||||
|
_closeSkinToneDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_closeSkinToneDialog();
|
||||||
|
_openSkinToneDialog(emoji, emojiSize, categoryEmoji, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return _buildButtonWidget(
|
||||||
|
onPressed: onPressed,
|
||||||
|
onLongPressed: onLongPressed,
|
||||||
|
child: _buildEmoji(
|
||||||
|
emojiSize,
|
||||||
|
categoryEmoji,
|
||||||
|
emoji,
|
||||||
|
widget.config.enableSkinTones,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build and display Emoji centered of its parent
|
||||||
|
Widget _buildEmoji(
|
||||||
|
double emojiSize,
|
||||||
|
CategoryEmoji categoryEmoji,
|
||||||
|
Emoji emoji,
|
||||||
|
bool showSkinToneIndicator,
|
||||||
|
) {
|
||||||
|
// FittedBox needed for display, font scale settings
|
||||||
|
return FittedBox(
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
child: Stack(children: [
|
||||||
|
emoji.hasSkinTone && showSkinToneIndicator
|
||||||
|
? Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: CustomPaint(
|
||||||
|
size: const Size(8, 8),
|
||||||
|
painter: TriangleShape(widget.config.skinToneIndicatorColor),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
Text(
|
||||||
|
emoji.emoji,
|
||||||
|
textScaleFactor: 1.0,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: emojiSize,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build different Button based on ButtonMode
|
||||||
|
Widget _buildButtonWidget({
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
required VoidCallback onLongPressed,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
if (widget.config.buttonMode == ButtonMode.MATERIAL) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
onLongPress: onLongPressed,
|
||||||
|
child: child,
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
minimumSize: MaterialStateProperty.all(Size.zero),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GestureDetector(
|
||||||
|
onLongPress: onLongPressed,
|
||||||
|
child: CupertinoButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build Widget for when no recent emoji are available
|
||||||
|
Widget _buildNoRecent() {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
widget.config.noRecentsText,
|
||||||
|
style: widget.config.noRecentsStyle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overlay for SkinTone
|
||||||
|
OverlayEntry _buildSkinToneOverlay(
|
||||||
|
Emoji emoji,
|
||||||
|
double emojiSize,
|
||||||
|
CategoryEmoji categoryEmoji,
|
||||||
|
int index,
|
||||||
|
) {
|
||||||
|
// Calculate position of emoji in the grid
|
||||||
|
final row = index ~/ widget.config.columns;
|
||||||
|
final column = index % widget.config.columns;
|
||||||
|
// Calculate position for skin tone dialog
|
||||||
|
final renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
final offset = renderBox.localToGlobal(Offset.zero);
|
||||||
|
final emojiSpace = renderBox.size.width / widget.config.columns;
|
||||||
|
final topOffset = emojiSpace;
|
||||||
|
final leftOffset = _getLeftOffset(emojiSpace, column);
|
||||||
|
final left = offset.dx + column * emojiSpace + leftOffset;
|
||||||
|
final top = tabBarHeight +
|
||||||
|
offset.dy +
|
||||||
|
row * emojiSpace -
|
||||||
|
_scrollController.offset -
|
||||||
|
topOffset;
|
||||||
|
|
||||||
|
// Generate other skintone options
|
||||||
|
final skinTonesEmoji = SkinTone.values
|
||||||
|
.map((skinTone) => _utils.applySkinTone(emoji, skinTone))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return OverlayEntry(
|
||||||
|
builder: (context) => Positioned(
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
color: widget.config.skinToneDialogBgColor,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildSkinToneEmoji(
|
||||||
|
categoryEmoji, emoji, emojiSpace, emojiSize),
|
||||||
|
_buildSkinToneEmoji(
|
||||||
|
categoryEmoji, skinTonesEmoji[0], emojiSpace, emojiSize),
|
||||||
|
_buildSkinToneEmoji(
|
||||||
|
categoryEmoji, skinTonesEmoji[1], emojiSpace, emojiSize),
|
||||||
|
_buildSkinToneEmoji(
|
||||||
|
categoryEmoji, skinTonesEmoji[2], emojiSpace, emojiSize),
|
||||||
|
_buildSkinToneEmoji(
|
||||||
|
categoryEmoji, skinTonesEmoji[3], emojiSpace, emojiSize),
|
||||||
|
_buildSkinToneEmoji(
|
||||||
|
categoryEmoji, skinTonesEmoji[4], emojiSpace, emojiSize),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Emoji inside skin tone dialog
|
||||||
|
Widget _buildSkinToneEmoji(
|
||||||
|
CategoryEmoji categoryEmoji,
|
||||||
|
Emoji emoji,
|
||||||
|
double width,
|
||||||
|
double emojiSize,
|
||||||
|
) {
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: width,
|
||||||
|
child: _buildButtonWidget(
|
||||||
|
onPressed: () {
|
||||||
|
widget.state.onEmojiSelected(categoryEmoji.category, emoji);
|
||||||
|
_closeSkinToneDialog();
|
||||||
|
},
|
||||||
|
onLongPressed: () {},
|
||||||
|
child: _buildEmoji(emojiSize, categoryEmoji, emoji, false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calucates the offset from the middle of selected emoji to the left side
|
||||||
|
// of the skin tone dialog
|
||||||
|
// Case 1: Selected Emoji is close to left border and offset needs to be
|
||||||
|
// reduced
|
||||||
|
// Case 2: Selected Emoji is close to right border and offset needs to be
|
||||||
|
// larger than half of the whole width
|
||||||
|
// Case 3: Enough space to left and right border and offset can be half
|
||||||
|
// of whole width
|
||||||
|
double _getLeftOffset(double emojiWidth, int column) {
|
||||||
|
final remainingColumns =
|
||||||
|
widget.config.columns - (column + 1 + (_skinToneCount ~/ 2));
|
||||||
|
if (column >= 0 && column < 3) {
|
||||||
|
return -1 * column * emojiWidth;
|
||||||
|
} else if (remainingColumns < 0) {
|
||||||
|
return -1 *
|
||||||
|
((_skinToneCount ~/ 2 - 1) + -1 * remainingColumns) *
|
||||||
|
emojiWidth;
|
||||||
|
}
|
||||||
|
return -1 * ((_skinToneCount ~/ 2) * emojiWidth) + emojiWidth / 2;
|
||||||
|
}
|
||||||
|
}
|
28
pubspec.lock
28
pubspec.lock
@ -388,6 +388,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
emoji_proposal:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: emoji_proposal
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
|
emojis:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: emojis
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.9.9"
|
||||||
encrypt:
|
encrypt:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1416,6 +1430,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.3"
|
version: "0.4.3"
|
||||||
|
remove_emoji:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: remove_emoji
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.7"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1437,6 +1458,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.2"
|
version: "0.3.2"
|
||||||
|
sentiment_dart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sentiment_dart
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.5"
|
||||||
sentry:
|
sentry:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -24,6 +24,8 @@ dependencies:
|
|||||||
dynamic_color: ^1.2.2
|
dynamic_color: ^1.2.2
|
||||||
email_validator: ^2.0.1
|
email_validator: ^2.0.1
|
||||||
emoji_picker_flutter: ^1.1.2
|
emoji_picker_flutter: ^1.1.2
|
||||||
|
emoji_proposal: ^0.0.1
|
||||||
|
emojis: ^0.9.0
|
||||||
encrypt: ^5.0.1
|
encrypt: ^5.0.1
|
||||||
#fcm_shared_isolate: ^0.1.0
|
#fcm_shared_isolate: ^0.1.0
|
||||||
file_picker_cross: ^4.6.0
|
file_picker_cross: ^4.6.0
|
||||||
|
@ -157,9 +157,9 @@ diff --git a/pubspec.yaml b/pubspec.yaml
|
|||||||
index 6999d0b8..b2c9144f 100644
|
index 6999d0b8..b2c9144f 100644
|
||||||
--- a/pubspec.yaml
|
--- a/pubspec.yaml
|
||||||
+++ b/pubspec.yaml
|
+++ b/pubspec.yaml
|
||||||
@@ -25,7 +25,7 @@ dependencies:
|
@@ -27,7 +27,7 @@ dependencies:
|
||||||
email_validator: ^2.0.1
|
emoji_proposal: ^0.0.1
|
||||||
emoji_picker_flutter: ^1.1.2
|
emojis: ^0.9.0
|
||||||
encrypt: ^5.0.1
|
encrypt: ^5.0.1
|
||||||
- #fcm_shared_isolate: ^0.1.0
|
- #fcm_shared_isolate: ^0.1.0
|
||||||
+ fcm_shared_isolate: ^0.1.0
|
+ fcm_shared_isolate: ^0.1.0
|
||||||
|
Loading…
Reference in New Issue
Block a user