mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-15 11:48:21 +00:00
@ -1,5 +1,4 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
import 'package:ndk/shared/nips/nip19/nip19.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/theme.dart';
|
||||||
import 'package:zap_stream_flutter/utils.dart';
|
import 'package:zap_stream_flutter/utils.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/avatar.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/profile.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/reaction.dart';
|
|
||||||
|
|
||||||
class WriteMessageWidget extends StatefulWidget {
|
class WriteMessageWidget extends StatefulWidget {
|
||||||
final StreamEvent stream;
|
final StreamEvent stream;
|
||||||
@ -24,6 +23,7 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
|||||||
late final TextEditingController _controller;
|
late final TextEditingController _controller;
|
||||||
OverlayEntry? _entry;
|
OverlayEntry? _entry;
|
||||||
late FocusNode _focusNode;
|
late FocusNode _focusNode;
|
||||||
|
List<List<String>> _tags = List.empty(growable: true);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -177,11 +177,24 @@ class __WriteMessageWidget extends State<WriteMessageWidget> {
|
|||||||
color: LAYER_2,
|
color: LAYER_2,
|
||||||
borderRadius: DEFAULT_BR,
|
borderRadius: DEFAULT_BR,
|
||||||
),
|
),
|
||||||
child: EmojiPicker(
|
child: EmojiPickerCustom(
|
||||||
onEmojiSelected: (category, emoji) {
|
customEmojiSets: [widget.stream.info.host],
|
||||||
_controller.text = _controller.text + emoji.emoji;
|
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();
|
final login = ndk.accounts.getLoggedAccount();
|
||||||
if (login == null || _controller.text.isEmpty) return;
|
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(
|
final chatMsg = Nip01Event(
|
||||||
pubKey: login.pubkey,
|
pubKey: login.pubkey,
|
||||||
kind: 1311,
|
kind: 1311,
|
||||||
content: _controller.text.toString(),
|
content: _controller.text.toString(),
|
||||||
tags: [
|
tags: tags,
|
||||||
["a", widget.stream.aTag],
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
_controller.clear();
|
_controller.clear();
|
||||||
_focusNode.unfocus();
|
_focusNode.unfocus();
|
||||||
|
161
lib/widgets/emoji.dart
Normal file
161
lib/widgets/emoji.dart
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
@ -61,6 +61,7 @@ List<InlineSpan> textToSpans(
|
|||||||
r'nostr:(note|nevent|naddr)[a-zA-Z0-9]+|'
|
r'nostr:(note|nevent|naddr)[a-zA-Z0-9]+|'
|
||||||
r'(#\$\$\s*[0-9]+\s*\$\$)|'
|
r'(#\$\$\s*[0-9]+\s*\$\$)|'
|
||||||
r'(#\w+)|' // Hashtags
|
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
|
r'(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,10}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*))', // URLs
|
||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
);
|
);
|
||||||
@ -76,28 +77,25 @@ List<InlineSpan> textToSpans(
|
|||||||
spans.add(_buildHashtagSpan(matched));
|
spans.add(_buildHashtagSpan(matched));
|
||||||
} else if (matched.startsWith('http')) {
|
} else if (matched.startsWith('http')) {
|
||||||
spans.add(_buildUrlSpan(matched, embedMedia ?? false));
|
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 '';
|
return '';
|
||||||
},
|
},
|
||||||
onNonMatch: (String text) {
|
onNonMatch: (String text) {
|
||||||
final textTrim = text.trim();
|
spans.add(TextSpan(text: text));
|
||||||
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));
|
|
||||||
}
|
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -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:flutter/widgets.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:ndk/entities.dart';
|
import 'package:ndk/entities.dart';
|
||||||
import 'package:zap_stream_flutter/const.dart';
|
import 'package:zap_stream_flutter/const.dart';
|
||||||
import 'package:zap_stream_flutter/theme.dart';
|
import 'package:zap_stream_flutter/widgets/emoji.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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
class ReactionWidget extends StatelessWidget {
|
class ReactionWidget extends StatelessWidget {
|
||||||
final Nip01Event event;
|
final Nip01Event event;
|
||||||
@ -38,15 +11,11 @@ class ReactionWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return EmojiPicker(
|
return EmojiPickerCustom(
|
||||||
onEmojiSelected: (category, emoji) {
|
onEmojiSelected: (e) {
|
||||||
ndk.broadcast.broadcastReaction(
|
ndk.broadcast.broadcastReaction(eventId: event.id, reaction: e.emoji);
|
||||||
eventId: event.id,
|
context.pop();
|
||||||
reaction: emoji.emoji,
|
|
||||||
);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
},
|
||||||
config: emojiPickerConfig,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
pubspec.lock
21
pubspec.lock
@ -220,10 +220,11 @@ packages:
|
|||||||
emoji_picker_flutter:
|
emoji_picker_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: emoji_picker_flutter
|
path: "."
|
||||||
sha256: "9a44c102079891ea5877f78c70f2e3c6e9df7b7fe0a01757d31f1046eeaa016d"
|
ref: HEAD
|
||||||
url: "https://pub.dev"
|
resolved-ref: "21a71ffed243ce88c625723d8f3ae0281a480b04"
|
||||||
source: hosted
|
url: "https://github.com/nostrlabs-io/emoji_picker_flutter"
|
||||||
|
source: git
|
||||||
version: "4.3.0"
|
version: "4.3.0"
|
||||||
equatable:
|
equatable:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@ -721,8 +722,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/ndk"
|
path: "packages/ndk"
|
||||||
ref: "9076bb42ed927314249165e59c7c3428268ce889"
|
ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
|
||||||
resolved-ref: "9076bb42ed927314249165e59c7c3428268ce889"
|
resolved-ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
|
||||||
url: "https://github.com/relaystr/ndk"
|
url: "https://github.com/relaystr/ndk"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.2"
|
version: "0.3.2"
|
||||||
@ -730,8 +731,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/amber"
|
path: "packages/amber"
|
||||||
ref: "9076bb42ed927314249165e59c7c3428268ce889"
|
ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
|
||||||
resolved-ref: "9076bb42ed927314249165e59c7c3428268ce889"
|
resolved-ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
|
||||||
url: "https://github.com/relaystr/ndk"
|
url: "https://github.com/relaystr/ndk"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.0"
|
version: "0.3.0"
|
||||||
@ -739,8 +740,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "packages/objectbox"
|
path: "packages/objectbox"
|
||||||
ref: "9076bb42ed927314249165e59c7c3428268ce889"
|
ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
|
||||||
resolved-ref: "9076bb42ed927314249165e59c7c3428268ce889"
|
resolved-ref: "6242899ee4ff7f65e57518d4eb29e2a253b3c4da"
|
||||||
url: "https://github.com/relaystr/ndk"
|
url: "https://github.com/relaystr/ndk"
|
||||||
source: git
|
source: git
|
||||||
version: "0.2.3"
|
version: "0.2.3"
|
||||||
|
@ -62,6 +62,9 @@ dependency_overrides:
|
|||||||
url: https://github.com/relaystr/ndk
|
url: https://github.com/relaystr/ndk
|
||||||
path: packages/amber
|
path: packages/amber
|
||||||
ref: 6242899ee4ff7f65e57518d4eb29e2a253b3c4da
|
ref: 6242899ee4ff7f65e57518d4eb29e2a253b3c4da
|
||||||
|
emoji_picker_flutter:
|
||||||
|
git:
|
||||||
|
url: https://github.com/nostrlabs-io/emoji_picker_flutter
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user