feat: improve chat modal

This commit is contained in:
2025-05-13 10:59:22 +01:00
parent 994b40dda9
commit fb32b1cfdb
6 changed files with 156 additions and 89 deletions

View File

@ -8,7 +8,7 @@ import 'package:zap_stream_flutter/utils.dart';
import 'package:zap_stream_flutter/widgets/avatar.dart';
import 'package:zap_stream_flutter/widgets/nostr_text.dart';
import 'package:zap_stream_flutter/widgets/profile.dart';
import 'package:zap_stream_flutter/widgets/profile_modal.dart';
import 'package:zap_stream_flutter/widgets/chat_modal.dart';
class ChatWidget extends StatelessWidget {
final StreamEvent stream;
@ -250,7 +250,7 @@ class _ChatMessageWidget extends StatelessWidget {
context: context,
constraints: BoxConstraints.expand(),
builder:
(ctx) => ProfileModalWidget(profile: profile, event: msg),
(ctx) => ChatModalWidget(profile: profile, event: msg),
);
}
},

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:ndk/ndk.dart';
import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/widgets/mute_button.dart';
import 'package:zap_stream_flutter/widgets/nostr_text.dart';
import 'package:zap_stream_flutter/widgets/profile.dart';
import 'package:zap_stream_flutter/widgets/reaction.dart';
import 'package:zap_stream_flutter/widgets/zap.dart';
class ChatModalWidget extends StatefulWidget {
final Metadata profile;
final Nip01Event event;
const ChatModalWidget({
super.key,
required this.profile,
required this.event,
});
@override
State<StatefulWidget> createState() => _ChatModalWidget();
}
class _ChatModalWidget extends State<ChatModalWidget> {
bool _showEmojiPicker = false;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(5, 10, 5, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 10,
children: [
ProfileWidget(profile: widget.profile),
Container(
width: double.maxFinite,
decoration: BoxDecoration(color: LAYER_2, borderRadius: DEFAULT_BR),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: NoteText(event: widget.event),
),
Row(
spacing: 8,
children: [
IconButton.filled(
color: LAYER_5,
style: ButtonStyle(
backgroundColor: WidgetStateColor.resolveWith((_) => LAYER_3),
),
onPressed:
() => setState(() {
_showEmojiPicker = !_showEmojiPicker;
}),
icon: Icon(Icons.mood),
),
IconButton.filled(
color: ZAP_1,
style: ButtonStyle(
backgroundColor: WidgetStateColor.resolveWith((_) => LAYER_3),
),
onPressed: () {
Navigator.pop(context);
showModalBottomSheet(
context: context,
builder: (ctx) {
return ZapWidget(
pubkey: widget.event.pubKey,
target: widget.event,
);
},
);
},
icon: Icon(Icons.bolt),
),
],
),
if (_showEmojiPicker) ReactionWidget(event: widget.event),
MuteButton(pubkey: widget.event.pubKey),
],
),
);
}
}

View File

@ -8,6 +8,21 @@ import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/utils.dart';
import 'package:zap_stream_flutter/widgets/profile.dart';
class NoteText extends StatelessWidget {
final Nip01Event event;
const NoteText({super.key, required this.event});
@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
children: textToSpans(event.content, event.tags, event.pubKey),
),
);
}
}
/// Converts a nostr note text containing links
/// and mentions into multiple spans for rendering
List<InlineSpan> textToSpans(

View File

@ -1,85 +0,0 @@
import 'dart:developer' as developer;
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/foundation.dart' as foundation;
import 'package:flutter/material.dart';
import 'package:ndk/ndk.dart';
import 'package:zap_stream_flutter/main.dart';
import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/widgets/button.dart';
import 'package:zap_stream_flutter/widgets/mute_button.dart';
import 'package:zap_stream_flutter/widgets/profile.dart';
import 'package:zap_stream_flutter/widgets/zap.dart';
class ProfileModalWidget extends StatelessWidget {
final Metadata profile;
final Nip01Event event;
const ProfileModalWidget({
super.key,
required this.profile,
required this.event,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(5, 10, 5, 0),
child: Column(
spacing: 10,
children: [
ProfileWidget(profile: profile),
EmojiPicker(
onEmojiSelected: (category, emoji) {
developer.log(emoji.emoji);
ndk.broadcast.broadcastReaction(
eventId: event.id,
reaction: emoji.emoji,
);
Navigator.pop(context);
},
config: 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,
),
categoryViewConfig: CategoryViewConfig(backgroundColor: LAYER_2),
searchViewConfig: SearchViewConfig(
backgroundColor: LAYER_2,
buttonIconColor: PRIMARY_1,
),
),
),
BasicButton.text(
"Zap",
onTap: () {
showModalBottomSheet(
context: context,
constraints: BoxConstraints.expand(),
builder: (ctx) {
return ZapWidget(pubkey: event.pubKey, target: event);
},
);
},
),
MuteButton(pubkey: event.pubKey),
],
),
);
}
}

51
lib/widgets/reaction.dart Normal file
View File

@ -0,0 +1,51 @@
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/foundation.dart' as foundation;
import 'package:flutter/widgets.dart';
import 'package:ndk/entities.dart';
import 'package:zap_stream_flutter/main.dart';
import 'package:zap_stream_flutter/theme.dart';
class ReactionWidget extends StatelessWidget {
final Nip01Event event;
const ReactionWidget({super.key, required this.event});
@override
Widget build(BuildContext context) {
return EmojiPicker(
onEmojiSelected: (category, emoji) {
ndk.broadcast.broadcastReaction(
eventId: event.id,
reaction: emoji.emoji,
);
Navigator.pop(context);
},
config: 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,
),
categoryViewConfig: CategoryViewConfig(backgroundColor: LAYER_2),
searchViewConfig: SearchViewConfig(
backgroundColor: LAYER_2,
buttonIconColor: PRIMARY_1,
),
),
);
}
}

View File

@ -84,6 +84,7 @@ class _ZapWidget extends State<ZapWidget> {
),
BasicButton.text(
"Zap",
decoration: BoxDecoration(color: LAYER_3, borderRadius: DEFAULT_BR),
onTap: () {
try {
_loadZap();
@ -147,7 +148,9 @@ class _ZapWidget extends State<ZapWidget> {
pubKey: widget.pubkey,
eventId: widget.target?.id,
addressableId:
widget.target != null && widget.target!.kind >= 30_000 && widget.target!.kind < 40_000
widget.target != null &&
widget.target!.kind >= 30_000 &&
widget.target!.kind < 40_000
? "${widget.target!.kind}:${widget.target!.pubKey}:${widget.target!.getDtag()!}"
: null,
relays: defaultRelays,
@ -174,7 +177,7 @@ class _ZapWidget extends State<ZapWidget> {
}),
child: Container(
decoration: BoxDecoration(
color: n == _amount ? LAYER_2 : LAYER_1,
color: n == _amount ? LAYER_4 : LAYER_3,
borderRadius: DEFAULT_BR,
),
alignment: AlignmentDirectional.center,