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/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<Nip01Event>(
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() ??
<String>{};
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)),
],
),
),
);
});

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,
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,