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:
TheOneWithTheBraid 2022-07-14 16:04:24 +02:00 committed by The one with the Braid
commit 4b5bba1457
7 changed files with 500 additions and 12 deletions

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:fluffychat/utils/fluffy_emoji_picker.dart';
import 'chat.dart';
class ChatEmojiPicker extends StatelessWidget {
@ -19,6 +20,7 @@ class ChatEmojiPicker extends StatelessWidget {
? EmojiPicker(
onEmojiSelected: controller.onEmojiSelected,
onBackspacePressed: controller.emojiPickerBackspace,
customWidget: (c, s) => FluffyEmojiPickerView(c, s),
)
: null,
);

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.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_typeahead/flutter_typeahead.dart';
import 'package:matrix/matrix.dart';
@ -54,7 +55,7 @@ class InputBar extends StatelessWidget {
final List<Map<String, String?>> ret = <Map<String, String?>>[];
const maxResults = 30;
final commandMatch = RegExp(r'^\/([\w]*)$').firstMatch(searchText);
final commandMatch = RegExp(r'^/(\w*)$').firstMatch(searchText);
if (commandMatch != null) {
final commandSearch = commandMatch[1]!.toLowerCase();
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);
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') {
final ratio = MediaQuery.of(context).devicePixelRatio;
final url = Uri.parse(suggestion['mxc'] ?? '').getThumbnail(
@ -282,10 +327,17 @@ class InputBar extends StatelessWidget {
if (suggestion['type'] == 'command') {
insertText = suggestion['name']! + ' ';
startText = replaceText.replaceAllMapped(
RegExp(r'^(\/[\w]*)$'),
RegExp(r'^(/\w*)$'),
(Match m) => '/' + insertText,
);
}
if (suggestion['type'] == 'emoji') {
insertText = suggestion['emoji']! + ' ';
startText = replaceText.replaceAllMapped(
suggestion['current_word']!,
(Match m) => insertText,
);
}
if (suggestion['type'] == 'emote') {
var isUnique = true;
final insertEmote = suggestion['name'];
@ -376,10 +428,8 @@ class InputBar extends StatelessWidget {
hideOnEmpty: true,
hideOnLoading: true,
keepSuggestionsOnSuggestionSelected: true,
debounceDuration: const Duration(
milliseconds:
50), // show suggestions after 50ms idle time (default is 300)
debounceDuration: const Duration(milliseconds: 50),
// show suggestions after 50ms idle time (default is 300)
textFieldConfiguration: TextFieldConfiguration(
minLines: minLines,
maxLines: maxLines,
@ -407,8 +457,8 @@ class InputBar extends StatelessWidget {
onSuggestionSelected: (Map<String, String?> suggestion) =>
insertSuggestion(context, suggestion),
errorBuilder: (BuildContext context, Object? error) => Container(),
loadingBuilder: (BuildContext context) =>
Container(), // fix loading briefly flickering a dark box
loadingBuilder: (BuildContext context) => Container(),
// fix loading briefly flickering a dark box
noItemsFoundBuilder: (BuildContext context) =>
Container(), // fix loading briefly showing no suggestions
),

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:emoji_proposal/emoji_proposal.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
@ -8,6 +9,7 @@ import 'package:fluffychat/pages/chat/chat.dart';
class ReactionsPicker extends StatelessWidget {
final ChatController controller;
const ReactionsPicker(this.controller, {Key? key}) : super(key: key);
@override
@ -26,7 +28,13 @@ class ReactionsPicker extends StatelessWidget {
if (!display) {
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
.aggregatedEvents(
controller.timeline!, RelationshipTypes.reaction)