mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-14 11:27:43 +00:00
205 lines
5.2 KiB
Dart
205 lines
5.2 KiB
Dart
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<T> extends StatefulWidget {
|
|
final List<Filter> filters;
|
|
final bool leaveOpen;
|
|
final Widget Function(BuildContext, List<T>?) builder;
|
|
final T Function(Nip01Event)? mapper;
|
|
final List<String>? relays;
|
|
|
|
const RxFilter(
|
|
Key? key, {
|
|
required this.filters,
|
|
this.leaveOpen = false,
|
|
required this.builder,
|
|
this.mapper,
|
|
this.relays,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _RxFilter<T>();
|
|
}
|
|
|
|
class _RxFilter<T> extends State<RxFilter<T>> {
|
|
late final RxFilterState<T> _state;
|
|
|
|
@override
|
|
void initState() {
|
|
_state = RxFilterState<T>(
|
|
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<T> extends ChangeNotifier
|
|
implements ValueListenable<List<T>?> {
|
|
final List<Filter> filters;
|
|
final bool leaveOpen;
|
|
final T Function(Nip01Event)? mapper;
|
|
final List<String>? relays;
|
|
HashMap<String, (int, T)>? _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<T>? get value =>
|
|
_events != null ? List<T>.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<T> extends StatelessWidget {
|
|
final Future<List<Filter>> Function() filterBuilder;
|
|
final bool leaveOpen;
|
|
final Widget Function(BuildContext, List<T>?) 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<List<Filter>>(
|
|
future: filterBuilder(),
|
|
builder: (ctx, data) {
|
|
if (data.hasData) {
|
|
return RxFilter<T>(
|
|
super.key!,
|
|
filters: data.data!,
|
|
mapper: mapper,
|
|
builder: builder,
|
|
);
|
|
} else {
|
|
return loadingWidget ?? SizedBox.shrink();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|