feat: stream goals display

closes #6
This commit is contained in:
2025-05-13 11:45:02 +01:00
parent fb32b1cfdb
commit f8a2df0097
5 changed files with 112 additions and 12 deletions

View File

@ -27,14 +27,21 @@ class NoVerify extends EventVerifier {
final ndkCache = DbObjectBox();
final eventVerifier = kDebugMode ? NoVerify() : RustEventVerifier();
var ndk = Ndk(NdkConfig(eventVerifier: eventVerifier, cache: ndkCache, bootstrapRelays: defaultRelays));
var ndk = Ndk(
NdkConfig(
eventVerifier: eventVerifier,
cache: ndkCache,
bootstrapRelays: defaultRelays,
engine: NdkEngine.JIT,
),
);
const userAgent = "zap.stream/1.0";
const defaultRelays = [
"wss://nos.lol",
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.snort.social"
"wss://relay.snort.social",
];
const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today"];

View File

@ -1,6 +1,7 @@
import 'package:bech32/bech32.dart';
import 'package:collection/collection.dart';
import 'package:convert/convert.dart';
import 'package:intl/intl.dart';
import 'package:ndk/ndk.dart';
import 'package:ndk/shared/nips/nip19/nip19.dart';
@ -42,6 +43,7 @@ class StreamInfo {
String? gameId;
GameInfo? gameInfo;
List<String> streams;
List<String>? relays;
StreamInfo({
this.id,
@ -126,6 +128,9 @@ StreamInfo extractStreamInfo(Nip01Event ev) {
matchTag(t, 'starts', (v) => ret.starts = int.tryParse(v));
matchTag(t, 'ends', (v) => ret.ends = int.tryParse(v));
matchTag(t, 'service', (v) => ret.service = v);
if (t[0] == "relays") {
ret.relays = t.slice(1);
}
}
var sortedTags = sortStreamTags(ev.tags);
@ -226,12 +231,13 @@ class Category {
List<Category> AllCategories = []; // Implement as needed
String formatSats(int n) {
final fmt = NumberFormat();
if (n >= 1000000) {
return "${(n / 1000000).toStringAsFixed(1)}M";
return "${fmt.format(n / 1000000)}M";
} else if (n >= 1000) {
return "${(n / 1000).toStringAsFixed(1)}k";
return "${fmt.format(n / 1000)}k";
} else {
return "$n";
return fmt.format(n);
}
}

View File

@ -23,9 +23,16 @@ class ChatWidget extends StatelessWidget {
}
return RxFilter<Nip01Event>(
key: Key("stream:chat:${stream.aTag}"),
relays: stream.info.relays,
filters: [
Filter(kinds: [1311, 9735], limit: 200, aTags: [stream.aTag]),
Filter(kinds: [Nip51List.kMute], authors: muteLists),
...(stream.info.goal != null
? [
Filter(kinds: [9041], ids: [stream.info.goal!]),
]
: []),
],
builder: (ctx, state) {
final mutedPubkeys =
@ -33,7 +40,9 @@ class ChatWidget extends StatelessWidget {
.where((e) => e.kind == Nip51List.kMute)
.map((e) => e.tags)
.expand((e) => e)
.where((e) => e[0] == "p")
.where(
(e) => e[0] == "p" && e[1] != stream.info.host,
) // cant mute host
.map((e) => e[1])
.toSet();
@ -50,11 +59,20 @@ class ChatWidget extends StatelessWidget {
.reversed
.toList();
final goal = filteredChat.firstWhereOrNull(
(e) => e.id == stream.info.goal,
);
final zaps =
filteredChat
.where((e) => e.kind == 9735)
.map((e) => ZapReceipt.fromEvent(e))
.toList();
return Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_TopZappersWidget(events: filteredChat),
if (zaps.isNotEmpty) _TopZappersWidget(events: zaps),
if (goal != null) _StreamGoalWidget(goal: goal),
Expanded(
child: ListView.builder(
reverse: true,
@ -108,8 +126,71 @@ class ChatWidget extends StatelessWidget {
}
}
class _StreamGoalWidget extends StatelessWidget {
final Nip01Event goal;
const _StreamGoalWidget({required this.goal});
@override
Widget build(BuildContext context) {
final max = int.parse(goal.getFirstTag("amount") ?? "1");
return Container(
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 4),
child: Column(
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(goal.content),
RxFilter<Nip01Event>(
filters: [
Filter(kinds: [9735], eTags: [goal.id]),
],
builder: (ctx, state) {
final zaps = (state ?? []).map((e) => ZapReceipt.fromEvent(e));
final totalZaps =
zaps.fold(0, (acc, v) => acc + (v.amountSats ?? 0)) * 1000;
final progress = totalZaps / max;
final q = MediaQuery.of(ctx);
return Stack(
children: [
Container(
height: 10,
decoration: BoxDecoration(
color: LAYER_2,
borderRadius: DEFAULT_BR,
),
),
Container(
height: 10,
width: (q.size.width * progress).clamp(1, q.size.width),
decoration: BoxDecoration(
color: ZAP_1,
borderRadius: DEFAULT_BR,
),
),
Positioned(
right: 2,
child: Text(
"Goal: ${formatSats((max / 1000).toInt())}",
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.w500,
),
),
),
],
);
},
),
],
),
);
}
}
class _TopZappersWidget extends StatelessWidget {
final List<Nip01Event> events;
final List<ZapReceipt> events;
const _TopZappersWidget({required this.events});
@ -117,8 +198,6 @@ class _TopZappersWidget extends StatelessWidget {
Widget build(BuildContext context) {
final topZaps =
events
.where((e) => e.kind == 9735)
.map((e) => ZapReceipt.fromEvent(e))
.fold(<String, int>{}, (acc, e) {
if (e.sender != null) {
acc[e.sender!] = (acc[e.sender!] ?? 0) + e.amountSats!;
@ -249,8 +328,7 @@ class _ChatMessageWidget extends StatelessWidget {
showModalBottomSheet(
context: context,
constraints: BoxConstraints.expand(),
builder:
(ctx) => ChatModalWidget(profile: profile, event: msg),
builder: (ctx) => ChatModalWidget(profile: profile, event: msg),
);
}
},

View File

@ -480,6 +480,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
js:
dependency: transitive
description:

View File

@ -31,6 +31,7 @@ dependencies:
image_picker: ^1.1.2
emoji_picker_flutter: ^4.3.0
bech32: ^0.2.2
intl: ^0.20.2
dependency_overrides:
ndk: