mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-16 11:58:50 +00:00
@ -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)),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
56
lib/widgets/mute_button.dart
Normal file
56
lib/widgets/mute_button.dart
Normal 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
43
lib/widgets/profile_modal.dart
Normal file
43
lib/widgets/profile_modal.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user