feat: chat context menu

ref #7
This commit is contained in:
2025-05-12 12:56:09 +01:00
parent a5aa8c5fa7
commit 0fcb773afc
4 changed files with 193 additions and 96 deletions

View File

@ -9,6 +9,7 @@ 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/profile.dart'; import 'package:zap_stream_flutter/widgets/profile.dart';
import 'package:zap_stream_flutter/widgets/profile_modal.dart';
class ChatWidget extends StatelessWidget { class ChatWidget extends StatelessWidget {
final StreamEvent stream; final StreamEvent stream;
@ -17,91 +18,77 @@ class ChatWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hostMuteList = ndk.lists.getSingleNip51List( var muteLists = [stream.info.host];
Nip51List.kMute, if (ndk.accounts.getPublicKey() != null) {
Bip340EventSigner(privateKey: null, publicKey: stream.info.host), muteLists.add(ndk.accounts.getPublicKey()!);
forceRefresh: true, }
);
final signer = ndk.accounts.getLoggedAccount()?.signer;
final myMuteList =
signer != null
? ndk.lists.getSingleNip51List(
Nip51List.kMute,
signer,
forceRefresh: true,
)
: Future.value(null);
return RxFilter<Nip01Event>( return RxFilter<Nip01Event>(
filters: [ filters: [
Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]), Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]),
Filter(kinds: [Nip51List.kMute], authors: muteLists),
], ],
builder: (ctx, state) { builder: (ctx, state) {
return FutureBuilder( final mutedPubkeys =
future: Future.wait([hostMuteList, myMuteList]), (state ?? [])
builder: (ctx, muteState) { .where((e) => e.kind == Nip51List.kMute)
final mutedPubkeys = .map((e) => e.tags)
muteState.data .expand((e) => e)
?.map((e) => e?.pubKeys) .where((e) => e[0] == "p")
.where((e) => e != null) .map((e) => e[1])
.expand((e) => e!) .toSet();
.map((e) => e.value)
.toSet() ??
<String>{};
final filteredChat = final filteredChat =
(state ?? []) (state ?? [])
.where((e) => !mutedPubkeys.contains(e.pubKey)) .where(
.toList(); (e) =>
!mutedPubkeys.contains(switch (e.kind) {
9735 => ZapReceipt.fromEvent(e).sender ?? e.pubKey,
_ => e.pubKey,
}),
)
.toList();
return Column( return Column(
spacing: 8, spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_TopZappersWidget(events: filteredChat), _TopZappersWidget(events: filteredChat),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
reverse: true, reverse: true,
child: Column( child: Column(
spacing: 8, spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: children:
filteredChat filteredChat
.sortedBy((c) => c.createdAt) .sortedBy((c) => c.createdAt)
.map( .map(
(c) => switch (c.kind) { (c) => switch (c.kind) {
1311 => ChatMessageWidget( 1311 => ChatMessageWidget(stream: stream, msg: c),
stream: stream, 9735 => _ChatZapWidget(stream: stream, zap: c),
msg: c, _ => SizedBox.shrink(),
), },
9735 => _ChatZapWidget( )
stream: stream, .toList(),
zap: c,
),
_ => SizedBox.shrink(),
},
)
.toList(),
),
),
), ),
if (stream.info.status == StreamStatus.live) ),
WriteMessageWidget(stream: stream), ),
if (stream.info.status == StreamStatus.ended) if (stream.info.status == StreamStatus.live)
Container( WriteMessageWidget(stream: stream),
padding: EdgeInsets.all(8), if (stream.info.status == StreamStatus.ended)
margin: EdgeInsets.symmetric(vertical: 8), Container(
width: double.maxFinite, padding: EdgeInsets.all(8),
alignment: Alignment.center, margin: EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(borderRadius: DEFAULT_BR), width: double.maxFinite,
child: Text( alignment: Alignment.center,
"STREAM ENDED", decoration: BoxDecoration(borderRadius: DEFAULT_BR),
style: TextStyle(fontWeight: FontWeight.bold), child: Text(
), "STREAM ENDED",
), style: TextStyle(fontWeight: FontWeight.bold),
], ),
); ),
}, ],
); );
}, },
); );
@ -243,27 +230,38 @@ class ChatMessageWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ProfileLoaderWidget(msg.pubKey, (ctx, state) { return ProfileLoaderWidget(msg.pubKey, (ctx, state) {
final profile = state.data ?? Metadata(pubKey: msg.pubKey); final profile = state.data ?? Metadata(pubKey: msg.pubKey);
return RichText( return GestureDetector(
text: TextSpan( onLongPress: () {
children: [ showModalBottomSheet(
WidgetSpan( context: context,
child: AvatarWidget(profile: profile, size: 24), constraints: BoxConstraints.expand(),
alignment: PlaceholderAlignment.middle, builder: (ctx) => ProfileModalWidget(profile: profile, event: msg),
), );
TextSpan(text: " "), },
WidgetSpan( child: RichText(
alignment: PlaceholderAlignment.middle, text: TextSpan(
child: ProfileNameWidget( children: [
profile: profile, WidgetSpan(
style: TextStyle( child: AvatarWidget(profile: profile, size: 24),
color: alignment: PlaceholderAlignment.middle,
msg.pubKey == stream.info.host ? PRIMARY_1 : SECONDARY_1, ),
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: " "), TextSpan(text: msg.content, style: TextStyle(color: FONT_COLOR)),
TextSpan(text: msg.content, style: TextStyle(color: FONT_COLOR)), ],
], ),
), ),
); );
}); });

View File

@ -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);
}
},
);
},
);
}
}

View File

@ -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),
],
),
);
}
}

View File

@ -147,7 +147,7 @@ class _ZapWidget extends State<ZapWidget> {
pubKey: widget.pubkey, pubKey: widget.pubkey,
eventId: widget.target?.id, eventId: widget.target?.id,
addressableId: 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()!}" ? "${widget.target!.kind}:${widget.target!.pubKey}:${widget.target!.getDtag()!}"
: null, : null,
relays: defaultRelays, relays: defaultRelays,