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,41 +18,35 @@ 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(
future: Future.wait([hostMuteList, myMuteList]),
builder: (ctx, muteState) {
final mutedPubkeys = final mutedPubkeys =
muteState.data (state ?? [])
?.map((e) => e?.pubKeys) .where((e) => e.kind == Nip51List.kMute)
.where((e) => e != null) .map((e) => e.tags)
.expand((e) => e!) .expand((e) => e)
.map((e) => e.value) .where((e) => e[0] == "p")
.toSet() ?? .map((e) => e[1])
<String>{}; .toSet();
final filteredChat = final filteredChat =
(state ?? []) (state ?? [])
.where((e) => !mutedPubkeys.contains(e.pubKey)) .where(
(e) =>
!mutedPubkeys.contains(switch (e.kind) {
9735 => ZapReceipt.fromEvent(e).sender ?? e.pubKey,
_ => e.pubKey,
}),
)
.toList(); .toList();
return Column( return Column(
@ -70,14 +65,8 @@ class ChatWidget extends StatelessWidget {
.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,
),
9735 => _ChatZapWidget(
stream: stream,
zap: c,
),
_ => SizedBox.shrink(), _ => SizedBox.shrink(),
}, },
) )
@ -103,8 +92,6 @@ class ChatWidget extends StatelessWidget {
); );
}, },
); );
},
);
} }
} }
@ -243,7 +230,15 @@ 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(
onLongPress: () {
showModalBottomSheet(
context: context,
constraints: BoxConstraints.expand(),
builder: (ctx) => ProfileModalWidget(profile: profile, event: msg),
);
},
child: RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
WidgetSpan( WidgetSpan(
@ -257,7 +252,9 @@ class ChatMessageWidget extends StatelessWidget {
profile: profile, profile: profile,
style: TextStyle( style: TextStyle(
color: color:
msg.pubKey == stream.info.host ? PRIMARY_1 : SECONDARY_1, msg.pubKey == stream.info.host
? PRIMARY_1
: SECONDARY_1,
), ),
), ),
), ),
@ -265,6 +262,7 @@ class ChatMessageWidget extends StatelessWidget {
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,