mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-15 11:48:21 +00:00
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
@ -16,11 +17,11 @@ class RxFilter<T> extends StatefulWidget {
|
|||||||
final List<String>? relays;
|
final List<String>? relays;
|
||||||
|
|
||||||
const RxFilter(
|
const RxFilter(
|
||||||
Key key, {
|
Key? key, {
|
||||||
required this.filters,
|
required this.filters,
|
||||||
|
this.leaveOpen = false,
|
||||||
required this.builder,
|
required this.builder,
|
||||||
this.mapper,
|
this.mapper,
|
||||||
this.leaveOpen = true,
|
|
||||||
this.relays,
|
this.relays,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -29,22 +30,79 @@ class RxFilter<T> extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RxFilter<T> extends State<RxFilter<T>> {
|
class _RxFilter<T> extends State<RxFilter<T>> {
|
||||||
late NdkResponse _response;
|
late final RxFilterState<T> _state;
|
||||||
late StreamSubscription _listener;
|
|
||||||
HashMap<String, (int, T)>? _events;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
_state = RxFilterState<T>(
|
||||||
developer.log("RX:SEDNING ${widget.filters}");
|
|
||||||
_response = ndk.requests.subscription(
|
|
||||||
filters: widget.filters,
|
filters: widget.filters,
|
||||||
explicitRelays: widget.relays,
|
leaveOpen: widget.leaveOpen,
|
||||||
cacheWrite: true
|
mapper: widget.mapper,
|
||||||
|
relays: widget.relays,
|
||||||
);
|
);
|
||||||
if (!widget.leaveOpen) {
|
/* 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((_) {
|
_response.future.then((_) {
|
||||||
developer.log("RX:CLOSING ${widget.filters}");
|
developer.log("RX:CLOSING $filters");
|
||||||
ndk.requests.closeSubscription(_response.requestId);
|
ndk.requests.closeSubscription(_response.requestId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -55,27 +113,34 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
|||||||
developer.log("RX:ERROR $e");
|
developer.log("RX:ERROR $e");
|
||||||
})
|
})
|
||||||
.listen((events) {
|
.listen((events) {
|
||||||
if (context.mounted) {
|
developer.log("RX:GOT ${events.length} events for $filters");
|
||||||
setState(() {
|
var didUpdate = false;
|
||||||
developer.log(
|
for (final ev in events) {
|
||||||
"RX:GOT ${events.length} events for ${widget.filters}",
|
if (_replaceInto(ev)) {
|
||||||
);
|
didUpdate = true;
|
||||||
events.forEach(_replaceInto);
|
}
|
||||||
});
|
}
|
||||||
|
if (didUpdate) {
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _replaceInto(Nip01Event ev) {
|
void insertEvent(Nip01Event ev) {
|
||||||
|
if (_replaceInto(ev)) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _replaceInto(Nip01Event ev) {
|
||||||
final evKey = _eventKey(ev);
|
final evKey = _eventKey(ev);
|
||||||
_events ??= HashMap();
|
_events ??= HashMap();
|
||||||
final existing = _events![evKey];
|
final existing = _events![evKey];
|
||||||
if (existing == null || existing.$1 < ev.createdAt) {
|
if (existing == null || existing.$1 < ev.createdAt) {
|
||||||
_events![evKey] = (
|
_events![evKey] = (ev.createdAt, mapper != null ? mapper!(ev) : ev as T);
|
||||||
ev.createdAt,
|
return true;
|
||||||
widget.mapper != null ? widget.mapper!(ev) : ev as T,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _eventKey(Nip01Event ev) {
|
String _eventKey(Nip01Event ev) {
|
||||||
@ -89,17 +154,15 @@ class _RxFilter<T> extends State<RxFilter<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
List<T>? get value =>
|
||||||
super.dispose();
|
_events != null ? List<T>.from(_events!.values.map((v) => v.$2)) : null;
|
||||||
|
|
||||||
developer.log("RX:CLOSING ${widget.filters}");
|
|
||||||
_listener.cancel();
|
|
||||||
ndk.requests.closeSubscription(_response.requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
void dispose() {
|
||||||
return widget.builder(context, _events?.values.map((v) => v.$2).toList());
|
developer.log("RX:CLOSING $filters");
|
||||||
|
_listener.cancel();
|
||||||
|
ndk.requests.closeSubscription(_response.requestId);
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ class ChatWidget extends StatelessWidget {
|
|||||||
final StreamEvent stream;
|
final StreamEvent stream;
|
||||||
|
|
||||||
const ChatWidget({super.key, required this.stream});
|
const ChatWidget({super.key, required this.stream});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var moderators = [stream.info.host];
|
var moderators = [stream.info.host];
|
||||||
@ -112,40 +113,32 @@ class ChatWidget extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
reverse: true,
|
reverse: true,
|
||||||
primary: true,
|
|
||||||
itemCount: filteredChat.length,
|
itemCount: filteredChat.length,
|
||||||
itemBuilder:
|
itemBuilder: (ctx, idx) {
|
||||||
(ctx, idx) => switch (filteredChat[idx].kind) {
|
final msg = filteredChat[idx];
|
||||||
1311 => ChatMessageWidget(
|
final widget = switch (msg.kind) {
|
||||||
key: Key("chat:${filteredChat[idx].id}"),
|
1311 => ChatMessageWidget(
|
||||||
stream: stream,
|
stream: stream,
|
||||||
msg: filteredChat[idx],
|
msg: msg,
|
||||||
badges:
|
badges:
|
||||||
badgeAwards[filteredChat[idx].pubKey]
|
badgeAwards[msg.pubKey]
|
||||||
?.map(
|
?.map(
|
||||||
(a) => ChatBadgeWidget.fromATag(
|
(a) => ChatBadgeWidget.fromATag(
|
||||||
a,
|
a,
|
||||||
key: Key("${filteredChat[idx].pubKey}:$a"),
|
key: Key("${msg.pubKey}:$a"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
1312 => ChatRaidMessage(
|
1312 => ChatRaidMessage(event: msg, stream: stream),
|
||||||
event: filteredChat[idx],
|
1314 => ChatTimeoutWidget(timeout: msg),
|
||||||
stream: stream,
|
9735 => ChatZapWidget(stream: stream, zap: msg),
|
||||||
),
|
8 => ChatBadgeAwardWidget(event: msg, stream: stream),
|
||||||
1314 => ChatTimeoutWidget(timeout: filteredChat[idx]),
|
_ => SizedBox(),
|
||||||
9735 => ChatZapWidget(
|
};
|
||||||
key: Key("chat:${filteredChat[idx].id}"),
|
|
||||||
stream: stream,
|
return widget;
|
||||||
zap: filteredChat[idx],
|
},
|
||||||
),
|
|
||||||
8 => ChatBadgeAwardWidget(
|
|
||||||
event: filteredChat[idx],
|
|
||||||
stream: stream,
|
|
||||||
),
|
|
||||||
_ => SizedBox(),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (stream.info.status == StreamStatus.live && !isChatDisabled)
|
if (stream.info.status == StreamStatus.live && !isChatDisabled)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:ndk/ndk.dart';
|
import 'package:ndk/ndk.dart';
|
||||||
import 'package:zap_stream_flutter/const.dart';
|
import 'package:zap_stream_flutter/const.dart';
|
||||||
import 'package:zap_stream_flutter/rx_filter.dart';
|
|
||||||
import 'package:zap_stream_flutter/theme.dart';
|
import 'package:zap_stream_flutter/theme.dart';
|
||||||
import 'package:zap_stream_flutter/utils.dart';
|
import 'package:zap_stream_flutter/utils.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/avatar.dart';
|
import 'package:zap_stream_flutter/widgets/avatar.dart';
|
||||||
@ -24,33 +23,47 @@ class ChatMessageWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ProfileLoaderWidget(msg.pubKey, (ctx, state) {
|
return FutureBuilder(
|
||||||
final profile = state.data ?? Metadata(pubKey: msg.pubKey);
|
future: ndk.metadata.loadMetadata(msg.pubKey),
|
||||||
return GestureDetector(
|
builder: (ctx, state) {
|
||||||
onLongPress: () {
|
final profile = state.data ?? Metadata(pubKey: msg.pubKey);
|
||||||
if (ndk.accounts.canSign) {
|
return GestureDetector(
|
||||||
showModalBottomSheet(
|
onLongPress: () {
|
||||||
context: context,
|
if (ndk.accounts.canSign) {
|
||||||
constraints: BoxConstraints.expand(),
|
showModalBottomSheet(
|
||||||
builder:
|
context: context,
|
||||||
(ctx) => ChatModalWidget(
|
constraints: BoxConstraints.expand(),
|
||||||
profile: profile,
|
builder:
|
||||||
event: msg,
|
(ctx) => ChatModalWidget(
|
||||||
stream: stream,
|
profile: profile,
|
||||||
),
|
event: msg,
|
||||||
);
|
stream: stream,
|
||||||
}
|
),
|
||||||
},
|
);
|
||||||
child: Padding(
|
}
|
||||||
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
|
},
|
||||||
child: Column(
|
child: Padding(
|
||||||
spacing: 2,
|
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [_chatText(profile), ChatReactions(msg: msg)],
|
spacing: 2,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_chatText(profile),
|
||||||
|
/*RxFilter<Nip01Event>(
|
||||||
|
Key(msg.id),
|
||||||
|
filters: [
|
||||||
|
Filter(kinds: [9735, 7], eTags: [msg.id]),
|
||||||
|
],
|
||||||
|
builder: (context, state) {
|
||||||
|
return ChatReactions(msg: msg, events: state ?? []);
|
||||||
|
},
|
||||||
|
),*/
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
}, key: Key("chat:${msg.id}:profile"));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _chatText(Metadata profile) {
|
Widget _chatText(Metadata profile) {
|
||||||
@ -91,25 +104,15 @@ class ChatMessageWidget extends StatelessWidget {
|
|||||||
|
|
||||||
class ChatReactions extends StatelessWidget {
|
class ChatReactions extends StatelessWidget {
|
||||||
final Nip01Event msg;
|
final Nip01Event msg;
|
||||||
|
final List<Nip01Event> events;
|
||||||
|
|
||||||
const ChatReactions({super.key, required this.msg});
|
const ChatReactions({super.key, required this.msg, required this.events});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RxFilter<Nip01Event>(
|
|
||||||
Key("chat:${msg.id}:reactions"),
|
|
||||||
filters: [
|
|
||||||
Filter(kinds: [9735, 7], eTags: [msg.id]),
|
|
||||||
],
|
|
||||||
builder: (ctx, data) => _chatReactions(ctx, data),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _chatReactions(BuildContext context, List<Nip01Event>? events) {
|
|
||||||
if ((events?.length ?? 0) == 0) return SizedBox.shrink();
|
|
||||||
|
|
||||||
// reactions must have e tag pointing to msg
|
// reactions must have e tag pointing to msg
|
||||||
final filteredEvents = events!.where((e) => e.getEId() == msg.id);
|
final filteredEvents = events.where((e) => e.getEId() == msg.id);
|
||||||
|
if (filteredEvents.isEmpty) return SizedBox.shrink();
|
||||||
final zaps = filteredEvents
|
final zaps = filteredEvents
|
||||||
.where((e) => e.kind == 9735)
|
.where((e) => e.kind == 9735)
|
||||||
.map((e) => ZapReceipt.fromEvent(e));
|
.map((e) => ZapReceipt.fromEvent(e));
|
||||||
|
Reference in New Issue
Block a user