feat: custom emoji picker

closes #14
This commit is contained in:
2025-05-27 12:22:28 +01:00
parent 8dae9a97f2
commit e9062f0265
6 changed files with 225 additions and 73 deletions

View File

@ -1,5 +1,4 @@
import 'package:collection/collection.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/material.dart';
import 'package:ndk/ndk.dart';
import 'package:ndk/shared/nips/nip19/nip19.dart';
@ -8,8 +7,8 @@ import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/utils.dart';
import 'package:zap_stream_flutter/widgets/avatar.dart';
import 'package:zap_stream_flutter/widgets/emoji.dart';
import 'package:zap_stream_flutter/widgets/profile.dart';
import 'package:zap_stream_flutter/widgets/reaction.dart';
class WriteMessageWidget extends StatefulWidget {
final StreamEvent stream;
@ -24,6 +23,7 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
late final TextEditingController _controller;
OverlayEntry? _entry;
late FocusNode _focusNode;
List<List<String>> _tags = List.empty(growable: true);
@override
void initState() {
@ -177,11 +177,24 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
color: LAYER_2,
borderRadius: DEFAULT_BR,
),
child: EmojiPicker(
onEmojiSelected: (category, emoji) {
_controller.text = _controller.text + emoji.emoji;
child: EmojiPickerCustom(
customEmojiSets: [widget.stream.info.host],
onEmojiSelected: (emoji) {
_controller.text =
_controller.text +
(emoji.emoji.startsWith("http")
? ":${emoji.name}:"
: emoji.emoji);
if (emoji.emoji.startsWith("http")) {
setState(() {
_tags =
[
..._tags,
["emoji", emoji.name, emoji.emoji],
].toList();
});
}
},
config: emojiPickerConfig,
),
),
),
@ -196,13 +209,20 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
final login = ndk.accounts.getLoggedAccount();
if (login == null || _controller.text.isEmpty) return;
var tags = [
["a", widget.stream.aTag],
..._tags.where(
(t) => switch (t[0]) {
"emoji" => _controller.text.contains(":${t[1]}:"),
_ => true,
},
),
];
final chatMsg = Nip01Event(
pubKey: login.pubkey,
kind: 1311,
content: _controller.text.toString(),
tags: [
["a", widget.stream.aTag],
],
tags: tags,
);
_controller.clear();
_focusNode.unfocus();

161
lib/widgets/emoji.dart Normal file
View File

@ -0,0 +1,161 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emoji;
import 'package:emoji_picker_flutter/locales/default_emoji_set_locale.dart';
import 'package:flutter/foundation.dart' as foundation;
import 'package:flutter/material.dart';
import 'package:ndk/ndk.dart';
import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/rx_filter.dart';
import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/utils.dart';
class EmojiPickerCustom extends StatelessWidget {
final List<String>? customEmojiSets;
final void Function(emoji.Emoji)? onEmojiSelected;
const EmojiPickerCustom({
super.key,
this.onEmojiSelected,
this.customEmojiSets,
});
Widget _picker(List<CustomEmojiSet> customEmojiTags) {
return emoji.EmojiPicker(
config: emoji.Config(
emojiSet: (locale) {
var ret = List<emoji.CategoryEmoji>.from(
getDefaultEmojiLocale(locale),
);
// append custom emoji
final custom = emoji.Category(name: "Custom", icon: Icons.star);
ret.add(
emoji.CategoryEmoji(
custom,
customEmojiTags
.map((a) => a.emoji)
.flattened
.map(
(e) => emoji.Emoji(
e.url,
e.name,
widget: CachedNetworkImage(
imageUrl: e.url,
height: 24,
width: 24,
),
),
)
.toList(),
),
);
return ret;
},
height: 256,
checkPlatformCompatibility: false,
emojiViewConfig: emoji.EmojiViewConfig(
emojiSizeMax:
28 *
(foundation.defaultTargetPlatform == TargetPlatform.iOS
? 1.20
: 1.0),
backgroundColor: LAYER_1,
),
viewOrderConfig: const emoji.ViewOrderConfig(
top: emoji.EmojiPickerItem.categoryBar,
middle: emoji.EmojiPickerItem.emojiView,
bottom: emoji.EmojiPickerItem.searchBar,
),
bottomActionBarConfig: emoji.BottomActionBarConfig(
backgroundColor: LAYER_2,
buttonColor: PRIMARY_1,
showBackspaceButton: false,
),
categoryViewConfig: emoji.CategoryViewConfig(
backgroundColor: LAYER_2,
recentTabBehavior: emoji.RecentTabBehavior.NONE,
),
searchViewConfig: emoji.SearchViewConfig(
backgroundColor: LAYER_2,
buttonIconColor: PRIMARY_1,
),
),
onEmojiSelected: (_, e) {
if (onEmojiSelected != null) {
onEmojiSelected!(e);
}
},
);
}
@override
Widget build(BuildContext context) {
var emojiPubkeys = customEmojiSets ?? [];
final signer = ndk.accounts.getLoggedAccount()?.signer;
if (signer == null) {
return SizedBox.fromSize();
}
emojiPubkeys.add(signer.getPublicKey());
return FutureBuilder(
future:
ndk.requests
.query(
filters: [
Filter(kinds: [Nip51List.kEmojis], authors: emojiPubkeys),
],
)
.future,
builder: (context, state) {
final sets =
state.data
?.map((a) => a.tags.where((b) => b[0] == "a"))
.flattened
.map((e) => e[1])
.toSet();
if (sets == null || sets.isEmpty) {
return _picker([]);
}
return RxFilter<Nip01Event>(
Key("emoji-picker"),
filters: sets.map(aTagToFilter).toList(),
builder: (context, state) {
return _picker(
(state ?? []).map((e) => CustomEmojiSet(event: e)).toList(),
);
},
);
},
);
}
}
class CustomEmojiSet {
final Nip01Event _event;
CustomEmojiSet({required Nip01Event event}) : _event = event;
String get title {
return _event.getFirstTag("title") ?? _event.getDtag()!;
}
List<CustomEmoji> get emoji {
return _event.tags
.where((t) => t[0] == "emoji")
.map(CustomEmoji.fromTag)
.toList();
}
}
class CustomEmoji {
final String name;
final String url;
CustomEmoji({required this.name, required this.url});
static CustomEmoji fromTag(List<String> tag) {
return CustomEmoji(name: tag[1], url: tag[2]);
}
}

View File

@ -61,6 +61,7 @@ List<InlineSpan> textToSpans(
r'nostr:(note|nevent|naddr)[a-zA-Z0-9]+|'
r'(#\$\$\s*[0-9]+\s*\$\$)|'
r'(#\w+)|' // Hashtags
r'(:\w+:)|' // custom emoji
r'(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,10}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*))', // URLs
caseSensitive: false,
);
@ -76,28 +77,25 @@ List<InlineSpan> textToSpans(
spans.add(_buildHashtagSpan(matched));
} else if (matched.startsWith('http')) {
spans.add(_buildUrlSpan(matched, embedMedia ?? false));
} else if (matched.startsWith(":") &&
matched.endsWith(":") &&
tags.any(
(t) =>
t[0] == "emoji" &&
t[1] == matched.substring(1, matched.length - 1),
)) {
spans.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: CustomEmoji(emoji: matched, tags: tags, size: 24),
),
);
}
}
return '';
},
onNonMatch: (String text) {
final textTrim = text.trim();
if (textTrim.startsWith(":") &&
textTrim.endsWith(":") &&
tags.any(
(t) =>
t[0] == "emoji" &&
t[1] == textTrim.substring(1, textTrim.length - 1),
)) {
spans.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: CustomEmoji(emoji: textTrim, tags: tags, size: 24),
),
);
} else {
spans.add(TextSpan(text: text));
}
spans.add(TextSpan(text: text));
return '';
},
);

View File

@ -1,35 +1,8 @@
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/foundation.dart' as foundation;
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:ndk/entities.dart';
import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/theme.dart';
final emojiPickerConfig = Config(
height: 256,
checkPlatformCompatibility: true,
emojiViewConfig: EmojiViewConfig(
emojiSizeMax:
28 *
(foundation.defaultTargetPlatform == TargetPlatform.iOS ? 1.20 : 1.0),
backgroundColor: LAYER_1,
),
viewOrderConfig: const ViewOrderConfig(
top: EmojiPickerItem.categoryBar,
middle: EmojiPickerItem.emojiView,
bottom: EmojiPickerItem.searchBar,
),
bottomActionBarConfig: BottomActionBarConfig(
backgroundColor: LAYER_2,
buttonColor: PRIMARY_1,
showBackspaceButton: false,
),
categoryViewConfig: CategoryViewConfig(backgroundColor: LAYER_2),
searchViewConfig: SearchViewConfig(
backgroundColor: LAYER_2,
buttonIconColor: PRIMARY_1,
),
);
import 'package:zap_stream_flutter/widgets/emoji.dart';
class ReactionWidget extends StatelessWidget {
final Nip01Event event;
@ -38,15 +11,11 @@ class ReactionWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return EmojiPicker(
onEmojiSelected: (category, emoji) {
ndk.broadcast.broadcastReaction(
eventId: event.id,
reaction: emoji.emoji,
);
Navigator.pop(context);
return EmojiPickerCustom(
onEmojiSelected: (e) {
ndk.broadcast.broadcastReaction(eventId: event.id, reaction: e.emoji);
context.pop();
},
config: emojiPickerConfig,
);
}
}

View File

@ -220,10 +220,11 @@ packages:
emoji_picker_flutter:
dependency: "direct main"
description:
name: emoji_picker_flutter
sha256: "9a44c102079891ea5877f78c70f2e3c6e9df7b7fe0a01757d31f1046eeaa016d"
url: "https://pub.dev"
source: hosted
path: "."
ref: HEAD
resolved-ref: "21a71ffed243ce88c625723d8f3ae0281a480b04"
url: "https://github.com/nostrlabs-io/emoji_picker_flutter"
source: git
version: "4.3.0"
equatable:
dependency: transitive
@ -721,8 +722,8 @@ packages:
dependency: "direct main"
description:
path: "packages/ndk"
ref: "9076bb42ed927314249165e59c7c3428268ce889"
resolved-ref: "9076bb42ed927314249165e59c7c3428268ce889"
ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
resolved-ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
url: "https://github.com/relaystr/ndk"
source: git
version: "0.3.2"
@ -730,8 +731,8 @@ packages:
dependency: "direct main"
description:
path: "packages/amber"
ref: "9076bb42ed927314249165e59c7c3428268ce889"
resolved-ref: "9076bb42ed927314249165e59c7c3428268ce889"
ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
resolved-ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
url: "https://github.com/relaystr/ndk"
source: git
version: "0.3.0"
@ -739,8 +740,8 @@ packages:
dependency: "direct main"
description:
path: "packages/objectbox"
ref: "9076bb42ed927314249165e59c7c3428268ce889"
resolved-ref: "9076bb42ed927314249165e59c7c3428268ce889"
ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
resolved-ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
url: "https://github.com/relaystr/ndk"
source: git
version: "0.2.3"

View File

@ -62,6 +62,9 @@ dependency_overrides:
url: https://github.com/relaystr/ndk
path: packages/amber
ref: 6242899ee4ff7f65e57518d4eb29e2a253b3c4da
emoji_picker_flutter:
git:
url: https://github.com/nostrlabs-io/emoji_picker_flutter
dev_dependencies:
flutter_test: