mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-15 11:48:21 +00:00
@ -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"];
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
Reference in New Issue
Block a user