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