import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../shared/relay/relay.dart'; /// In-memory cache of other users' presence. /// /// Subscribes to kind:20001 presence events over the relay WebSocket for /// real-time updates. There is no longer a REST backstop — agents that /// publish presence purely over WS are fine, and TTL expiry will be handled /// by the relay-side `presence:true` filter extension when that lands. class PresenceCacheNotifier extends Notifier> { final Set _tracked = {}; void Function()? _presenceUnsub; int _subscriptionVersion = 0; @override Map build() { final sessionState = ref.watch(relaySessionProvider); ref.onDispose(() { _presenceUnsub?.call(); _presenceUnsub = null; }); if (sessionState.status == SessionStatus.connected) { _subscribePresenceUpdates(); } return {}; } /// Track presence for [pubkeys]. /// /// Currently a no-op for the actual fetch — we rely on live kind:20001 /// events. The tracked set is still used to filter incoming events so the /// cache doesn't grow unbounded. void track(List pubkeys) { final normalized = pubkeys.map((pk) => pk.toLowerCase()).toList(); _tracked.addAll(normalized); // TODO(presence): once the relay supports a `presence:true` filter // extension, issue a one-shot fetch here for the latest known state per // pubkey. Until then, presence is "online whenever they publish". } /// Subscribe to kind:20001 presence events over WebSocket. Future _subscribePresenceUpdates() async { _presenceUnsub?.call(); _presenceUnsub = null; _subscriptionVersion++; final version = _subscriptionVersion; final session = ref.read(relaySessionProvider.notifier); try { final unsub = await session.subscribe( const NostrFilter(kinds: [EventKind.presenceUpdate], limit: 0), _handlePresenceEvent, ); // Guard: if build() re-fired while we were awaiting, discard this // subscription to avoid leaking it. if (version != _subscriptionVersion) { unsub(); return; } _presenceUnsub = unsub; } catch (error) { debugPrint( '[PresenceCacheNotifier] presence subscription failed: $error', ); } } void _handlePresenceEvent(NostrEvent event) { final pubkey = event.pubkey.toLowerCase(); if (!_tracked.contains(pubkey)) return; final status = event.content; if (status != 'online' && status != 'away' && status != 'offline') return; if (state[pubkey] == status) return; final updated = Map.from(state); updated[pubkey] = status; state = updated; } } final presenceCacheProvider = NotifierProvider>( PresenceCacheNotifier.new, );