refactor: disable reactions due to bad UX

ref #36
This commit is contained in:
2025-05-23 15:54:52 +01:00
parent 8f41ed6d0e
commit 46b809ff58
3 changed files with 166 additions and 107 deletions

View File

@ -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();
} }
} }

View File

@ -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)

View File

@ -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));