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
5f5a08f729
commit
4b5bba1457
7 changed files with 500 additions and 12 deletions
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue