From 0fcb773afccb9fc18abc46fc9ed3356a3ecbd5e1 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 12 May 2025 12:56:09 +0100 Subject: [PATCH] feat: chat context menu ref #7 --- lib/widgets/chat.dart | 188 ++++++++++++++++----------------- lib/widgets/mute_button.dart | 56 ++++++++++ lib/widgets/profile_modal.dart | 43 ++++++++ lib/widgets/zap.dart | 2 +- 4 files changed, 193 insertions(+), 96 deletions(-) create mode 100644 lib/widgets/mute_button.dart create mode 100644 lib/widgets/profile_modal.dart diff --git a/lib/widgets/chat.dart b/lib/widgets/chat.dart index b137791..647c76d 100644 --- a/lib/widgets/chat.dart +++ b/lib/widgets/chat.dart @@ -9,6 +9,7 @@ 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/profile.dart'; +import 'package:zap_stream_flutter/widgets/profile_modal.dart'; class ChatWidget extends StatelessWidget { final StreamEvent stream; @@ -17,91 +18,77 @@ class ChatWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final hostMuteList = ndk.lists.getSingleNip51List( - Nip51List.kMute, - Bip340EventSigner(privateKey: null, publicKey: stream.info.host), - forceRefresh: true, - ); - final signer = ndk.accounts.getLoggedAccount()?.signer; - final myMuteList = - signer != null - ? ndk.lists.getSingleNip51List( - Nip51List.kMute, - signer, - forceRefresh: true, - ) - : Future.value(null); + var muteLists = [stream.info.host]; + if (ndk.accounts.getPublicKey() != null) { + muteLists.add(ndk.accounts.getPublicKey()!); + } return RxFilter( filters: [ Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]), + Filter(kinds: [Nip51List.kMute], authors: muteLists), ], builder: (ctx, state) { - return FutureBuilder( - future: Future.wait([hostMuteList, myMuteList]), - builder: (ctx, muteState) { - final mutedPubkeys = - muteState.data - ?.map((e) => e?.pubKeys) - .where((e) => e != null) - .expand((e) => e!) - .map((e) => e.value) - .toSet() ?? - {}; + final mutedPubkeys = + (state ?? []) + .where((e) => e.kind == Nip51List.kMute) + .map((e) => e.tags) + .expand((e) => e) + .where((e) => e[0] == "p") + .map((e) => e[1]) + .toSet(); - final filteredChat = - (state ?? []) - .where((e) => !mutedPubkeys.contains(e.pubKey)) - .toList(); + final filteredChat = + (state ?? []) + .where( + (e) => + !mutedPubkeys.contains(switch (e.kind) { + 9735 => ZapReceipt.fromEvent(e).sender ?? e.pubKey, + _ => e.pubKey, + }), + ) + .toList(); - return Column( - spacing: 8, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _TopZappersWidget(events: filteredChat), - Expanded( - child: SingleChildScrollView( - reverse: true, - child: Column( - spacing: 8, - crossAxisAlignment: CrossAxisAlignment.start, - children: - filteredChat - .sortedBy((c) => c.createdAt) - .map( - (c) => switch (c.kind) { - 1311 => ChatMessageWidget( - stream: stream, - msg: c, - ), - 9735 => _ChatZapWidget( - stream: stream, - zap: c, - ), - _ => SizedBox.shrink(), - }, - ) - .toList(), - ), - ), + return Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _TopZappersWidget(events: filteredChat), + Expanded( + child: SingleChildScrollView( + reverse: true, + child: Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, + children: + filteredChat + .sortedBy((c) => c.createdAt) + .map( + (c) => switch (c.kind) { + 1311 => ChatMessageWidget(stream: stream, msg: c), + 9735 => _ChatZapWidget(stream: stream, zap: c), + _ => SizedBox.shrink(), + }, + ) + .toList(), ), - if (stream.info.status == StreamStatus.live) - WriteMessageWidget(stream: stream), - if (stream.info.status == StreamStatus.ended) - Container( - padding: EdgeInsets.all(8), - margin: EdgeInsets.symmetric(vertical: 8), - width: double.maxFinite, - alignment: Alignment.center, - decoration: BoxDecoration(borderRadius: DEFAULT_BR), - child: Text( - "STREAM ENDED", - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ], - ); - }, + ), + ), + if (stream.info.status == StreamStatus.live) + WriteMessageWidget(stream: stream), + if (stream.info.status == StreamStatus.ended) + Container( + padding: EdgeInsets.all(8), + margin: EdgeInsets.symmetric(vertical: 8), + width: double.maxFinite, + alignment: Alignment.center, + decoration: BoxDecoration(borderRadius: DEFAULT_BR), + child: Text( + "STREAM ENDED", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ], ); }, ); @@ -243,27 +230,38 @@ class ChatMessageWidget extends StatelessWidget { Widget build(BuildContext context) { return ProfileLoaderWidget(msg.pubKey, (ctx, state) { final profile = state.data ?? Metadata(pubKey: msg.pubKey); - return RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: AvatarWidget(profile: profile, size: 24), - alignment: PlaceholderAlignment.middle, - ), - TextSpan(text: " "), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: ProfileNameWidget( - profile: profile, - style: TextStyle( - color: - msg.pubKey == stream.info.host ? PRIMARY_1 : SECONDARY_1, + return GestureDetector( + onLongPress: () { + showModalBottomSheet( + context: context, + constraints: BoxConstraints.expand(), + builder: (ctx) => ProfileModalWidget(profile: profile, event: msg), + ); + }, + child: RichText( + text: TextSpan( + children: [ + WidgetSpan( + child: AvatarWidget(profile: profile, size: 24), + alignment: PlaceholderAlignment.middle, + ), + TextSpan(text: " "), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: ProfileNameWidget( + profile: profile, + style: TextStyle( + color: + msg.pubKey == stream.info.host + ? PRIMARY_1 + : SECONDARY_1, + ), ), ), - ), - TextSpan(text: " "), - TextSpan(text: msg.content, style: TextStyle(color: FONT_COLOR)), - ], + TextSpan(text: " "), + TextSpan(text: msg.content, style: TextStyle(color: FONT_COLOR)), + ], + ), ), ); }); diff --git a/lib/widgets/mute_button.dart b/lib/widgets/mute_button.dart new file mode 100644 index 0000000..3832575 --- /dev/null +++ b/lib/widgets/mute_button.dart @@ -0,0 +1,56 @@ +import 'package:flutter/widgets.dart'; +import 'package:ndk/domain_layer/entities/nip_51_list.dart'; +import 'package:zap_stream_flutter/main.dart'; +import 'package:zap_stream_flutter/theme.dart'; +import 'package:zap_stream_flutter/widgets/button.dart'; + +class MuteButton extends StatelessWidget { + final String pubkey; + + const MuteButton({super.key, required this.pubkey}); + + @override + Widget build(BuildContext context) { + final signer = ndk.accounts.getLoggedAccount()?.signer; + if (signer == null) return SizedBox.shrink(); + + return FutureBuilder( + future: ndk.lists.getSingleNip51List(Nip51List.kMute, signer), + builder: (ctx, state) { + final mutes = (state.data?.pubKeys ?? []).map((e) => e.value).toSet(); + final isMuted = mutes.contains(pubkey); + return BasicButton( + Text( + isMuted ? "Unmute" : "Mute", + style: TextStyle( + color: Color.fromARGB(255, 0, 0, 0), + fontWeight: FontWeight.bold, + ), + ), + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 12), + decoration: BoxDecoration(color: WARNING, borderRadius: DEFAULT_BR), + onTap: () async { + if (isMuted) { + await ndk.lists.broadcastRemoveNip51ListElement( + Nip51List.kMute, + Nip51List.kPubkey, + pubkey, + null, + ); + } else { + await ndk.lists.broadcastAddNip51ListElement( + Nip51List.kMute, + Nip51List.kPubkey, + pubkey, + null, + ); + } + if (ctx.mounted) { + Navigator.pop(ctx); + } + }, + ); + }, + ); + } +} diff --git a/lib/widgets/profile_modal.dart b/lib/widgets/profile_modal.dart new file mode 100644 index 0000000..bda9207 --- /dev/null +++ b/lib/widgets/profile_modal.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:ndk/ndk.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), + BasicButton.text( + "Zap", + onTap: () { + showModalBottomSheet( + context: context, + constraints: BoxConstraints.expand(), + builder: (ctx) { + return ZapWidget(pubkey: event.pubKey, target: event); + }, + ); + }, + ), + MuteButton(pubkey: event.pubKey), + ], + ), + ); + } +} diff --git a/lib/widgets/zap.dart b/lib/widgets/zap.dart index 7784638..c0cb269 100644 --- a/lib/widgets/zap.dart +++ b/lib/widgets/zap.dart @@ -147,7 +147,7 @@ class _ZapWidget extends State { pubKey: widget.pubkey, eventId: widget.target?.id, addressableId: - widget.target != null + 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,