feat: tag zap goals

This commit is contained in:
2025-05-13 12:20:59 +01:00
parent f1e518a0d7
commit 3e18f7544e
4 changed files with 109 additions and 49 deletions

View File

@ -120,6 +120,13 @@ class _StreamPage extends State<StreamPage> {
return ZapWidget(
pubkey: widget.stream.info.host,
target: widget.stream.event,
zapTags:
// tag goal onto zap request
widget.stream.info.goal != null
? [
["e", widget.stream.info.goal!],
]
: null,
);
},
);

View File

@ -234,7 +234,7 @@ String formatSats(int n) {
final fmt = NumberFormat();
if (n >= 1000000) {
return "${fmt.format(n / 1000000)}M";
} else if (n >= 1000) {
} else if (n >= 1500) {
return "${fmt.format(n / 1000)}k";
} else {
return fmt.format(n);

View File

@ -136,23 +136,33 @@ class _StreamGoalWidget extends StatelessWidget {
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;
child: 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 remaining = ((max - totalZaps).clamp(0, max) / 1000).toInt();
final q = MediaQuery.of(ctx);
return Stack(
final q = MediaQuery.of(ctx);
return Column(
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(child: Text(goal.content)),
if (remaining > 0)
Text(
"Remaining: ${formatSats(remaining)}",
style: TextStyle(fontSize: 12, color: LAYER_5),
),
],
),
Stack(
children: [
Container(
height: 10,
@ -169,21 +179,33 @@ class _StreamGoalWidget extends StatelessWidget {
borderRadius: DEFAULT_BR,
),
),
Positioned(
right: 2,
child: Text(
"Goal: ${formatSats((max / 1000).toInt())}",
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.w500,
if (remaining > 0)
Positioned(
right: 2,
child: Text(
"Goal: ${formatSats((max / 1000).toInt())}",
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
),
if (remaining == 0)
Center(
child: Text(
"COMPLETE",
style: TextStyle(
color: LAYER_0,
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
},
),
],
),
],
);
},
),
);
}

View File

@ -1,4 +1,5 @@
import 'package:clipboard/clipboard.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:ndk/domain_layer/usecases/lnurl/lnurl.dart';
import 'package:ndk/ndk.dart';
@ -13,8 +14,16 @@ import 'package:zap_stream_flutter/widgets/profile.dart';
class ZapWidget extends StatefulWidget {
final String pubkey;
final Nip01Event? target;
final List<Nip01Event>? otherTargets;
final List<List<String>>? zapTags;
const ZapWidget({super.key, required this.pubkey, this.target});
const ZapWidget({
super.key,
required this.pubkey,
this.target,
this.zapTags,
this.otherTargets,
});
@override
State<StatefulWidget> createState() => _ZapWidget();
@ -133,31 +142,53 @@ class _ZapWidget extends State<ZapWidget> {
];
}
Future<ZapRequest?> _makeZap() async {
final signer = ndk.accounts.getLoggedAccount()?.signer;
if (signer == null) return null;
var relays = defaultRelays;
// if target event has relays tag, use that for zap
if (widget.target?.tags.any((t) => t[0] == "relays") ?? false) {
relays = widget.target!.tags.firstWhere((t) => t[0] == "relays").slice(1);
}
final amount = _amount! * 1000;
var tags = [
["relays", ...relays],
["amount", amount.toString()],
["p", widget.pubkey],
];
// tag targets for zap request
for (final t in [
...(widget.target != null ? [widget.target!] : []),
...(widget.otherTargets != null ? widget.otherTargets! : []),
]) {
if (t.kind >= 30_000 && t.kind < 40_000) {
tags.add(["a", "${t.kind}:${t.pubKey}:${t.getDtag()!}"]);
} else {
tags.add(["e", t.id]);
}
}
if (widget.zapTags != null) {
tags.addAll(widget.zapTags!);
}
var event = ZapRequest(
pubKey: signer.getPublicKey(),
tags: tags,
content: _comment.text,
);
await signer.sign(event);
return event;
}
Future<void> _loadZap() async {
final profile = await ndk.metadata.loadMetadata(widget.pubkey);
if (profile?.lud16 == null) {
throw "No lightning address found";
}
final signer = ndk.accounts.getLoggedAccount()?.signer;
final zapRequest =
signer != null
? await ndk.zaps.createZapRequest(
amountSats: _amount!,
signer: signer,
pubKey: widget.pubkey,
eventId: widget.target?.id,
addressableId:
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,
comment: _comment.text.isNotEmpty ? _comment.text : null,
)
: null;
final zapRequest = await _makeZap();
final invoice = await ndk.zaps.fetchInvoice(
lud16Link: Lnurl.getLud16LinkFromLud16(profile!.lud16!)!,
amountSats: _amount!,