import 'dart:async'; import 'dart:collection'; import 'dart:developer' as developer; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:ndk/ndk.dart'; import 'package:rxdart/rxdart.dart'; import 'package:zap_stream_flutter/const.dart'; /// Reactive filter which builds the widget with a snapshot of the data class RxFilter extends StatefulWidget { final List filters; final bool leaveOpen; final Widget Function(BuildContext, List?) builder; final T Function(Nip01Event)? mapper; final List? relays; const RxFilter( Key? key, { required this.filters, this.leaveOpen = false, required this.builder, this.mapper, this.relays, }) : super(key: key); @override State createState() => _RxFilter(); } class _RxFilter extends State> { late final RxFilterState _state; @override void initState() { _state = RxFilterState( filters: widget.filters, leaveOpen: widget.leaveOpen, mapper: widget.mapper, relays: widget.relays, ); /* send spam into chat if (widget.key is ValueKey) { final vk = (widget.key as ValueKey).value as String; if (vk.startsWith("stream:chat:")) { Timer.periodic(Duration(seconds: 1), (_) { final spam = Nip01Event( pubKey: "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", kind: 1311, tags: [ ["a", vk.split(":").last], ], content: "SPAM ${DateTime.now()}", ); _state.insertEvent(spam); }); } }*/ super.initState(); } @override void dispose() { _state.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: _state, builder: (context, state, _) { return widget.builder(context, state); }, ); } } class RxFilterState extends ChangeNotifier implements ValueListenable?> { final List filters; final bool leaveOpen; final T Function(Nip01Event)? mapper; final List? relays; HashMap? _events; late final NdkResponse _response; late final StreamSubscription _listener; RxFilterState({ required this.filters, this.leaveOpen = false, this.mapper, this.relays, }) { developer.log("RX:SEDNING $filters"); _response = ndk.requests.subscription( filters: filters, explicitRelays: relays, cacheWrite: true, ); if (!leaveOpen) { _response.future.then((_) { developer.log("RX:CLOSING $filters"); ndk.requests.closeSubscription(_response.requestId); }); } _listener = _response.stream .bufferTime(const Duration(milliseconds: 500)) .where((events) => events.isNotEmpty) .handleError((e) { developer.log("RX:ERROR $e"); }) .listen((events) { developer.log("RX:GOT ${events.length} events for $filters"); var didUpdate = false; for (final ev in events) { if (_replaceInto(ev)) { didUpdate = true; } } if (didUpdate) { notifyListeners(); } }); } void insertEvent(Nip01Event ev) { if (_replaceInto(ev)) { notifyListeners(); } } bool _replaceInto(Nip01Event ev) { final evKey = _eventKey(ev); _events ??= HashMap(); final existing = _events![evKey]; if (existing == null || existing.$1 < ev.createdAt) { _events![evKey] = (ev.createdAt, mapper != null ? mapper!(ev) : ev as T); return true; } return false; } String _eventKey(Nip01Event ev) { if ([0, 3].contains(ev.kind) || (ev.kind >= 10000 && ev.kind < 20000)) { return "${ev.kind}:${ev.pubKey}"; } else if (ev.kind >= 30000 && ev.kind < 40000) { return "${ev.kind}:${ev.pubKey}:${ev.getDtag()}"; } else { return ev.id; } } @override List? get value => _events != null ? List.from(_events!.values.map((v) => v.$2)) : null; @override void dispose() { developer.log("RX:CLOSING $filters"); _listener.cancel(); ndk.requests.closeSubscription(_response.requestId); super.dispose(); } } /// An async filter loader into [RxFilter] class RxFutureFilter extends StatelessWidget { final Future> Function() filterBuilder; final bool leaveOpen; final Widget Function(BuildContext, List?) builder; final Widget? loadingWidget; final T Function(Nip01Event)? mapper; const RxFutureFilter( Key key, { required this.filterBuilder, required this.builder, this.mapper, this.leaveOpen = true, this.loadingWidget, }) : super(key: key); @override Widget build(BuildContext context) { return FutureBuilder>( future: filterBuilder(), builder: (ctx, data) { if (data.hasData) { return RxFilter( super.key!, filters: data.data!, mapper: mapper, builder: builder, ); } else { return loadingWidget ?? SizedBox.shrink(); } }, ); } }