Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion mobile/lib/shared/community/community_membership_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,25 @@ CommunityMembershipSnapshot communityMembershipFromEvents(
/// The HTTP query resolves from the first response rather than waiting for a
/// WebSocket EOSE frame, and watching [relayConfigProvider] makes the result
/// community-scoped.
///
/// The snapshot is queried once per community and kept alive across consumer
/// remounts. It refreshes only on an explicit `ref.invalidate`, on a community
/// switch, or when the relay session (re)connects — never on every session
/// state emission.
final communityMembershipProvider =
FutureProvider.autoDispose<CommunityMembershipSnapshot>((ref) async {
ref.watch(relayConfigProvider);
final session = ref.watch(relaySessionProvider.notifier);
ref.keepAlive();
// Re-fetch only after a (re)connect completes, mirroring
// channelMembersProvider. Reading (not watching) the notifier keeps
// reconnecting/disconnected transitions from rebuilding this provider.
ref.listen(relaySessionProvider, (previous, next) {
if (next.status == SessionStatus.connected &&
previous?.status != SessionStatus.connected) {
ref.invalidateSelf();
}
});
final session = ref.read(relaySessionProvider.notifier);
final events = await session.queryRelay([NostrFilters.relayMembers()]);
return communityMembershipFromEvents(events);
});
Expand Down
119 changes: 119 additions & 0 deletions mobile/test/shared/community/community_membership_provider_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:buzz/shared/community/community_membership_provider.dart';
import 'package:buzz/shared/relay/relay.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
const owner =
Expand Down Expand Up @@ -58,6 +59,124 @@ void main() {
expect(snapshot.members, isEmpty);
expect(canManageCommunityInvites(snapshot.roleFor(owner)), isFalse);
});

group('communityMembershipProvider caching', () {
late _CountingRelaySession session;
late ProviderContainer container;

setUp(() {
session = _CountingRelaySession(owner);
container = ProviderContainer(
retry: (_, _) => null,
overrides: [
relaySessionProvider.overrideWith(() => session),
relayConfigProvider.overrideWith(_TestRelayConfigNotifier.new),
],
);
addTearDown(container.dispose);
});

test('does not re-query when the session state changes', () async {
final subscription = container.listen(
communityMembershipProvider,
(_, _) {},
);
addTearDown(subscription.close);

final first = await container.read(communityMembershipProvider.future);
expect(first.roleFor(owner), CommunityMemberRole.owner);
expect(session.queryCount, 1);

session.setStatus(SessionStatus.reconnecting);
await container.pump();
session.setStatus(SessionStatus.disconnected);
await container.pump();
await container.read(communityMembershipProvider.future);

expect(session.queryCount, 1);
final cached = container.read(communityMembershipProvider).asData?.value;
expect(cached?.roleFor(owner), CommunityMemberRole.owner);
});

test('does not re-query when a consumer remounts', () async {
final subscription = container.listen(
communityMembershipProvider,
(_, _) {},
);
await container.read(communityMembershipProvider.future);
expect(session.queryCount, 1);

subscription.close();
await container.pump();

final remounted = container.listen(
communityMembershipProvider,
(_, _) {},
);
addTearDown(remounted.close);
await container.read(communityMembershipProvider.future);

expect(session.queryCount, 1);
});

test('re-queries on explicit invalidation and on reconnect', () async {
final subscription = container.listen(
communityMembershipProvider,
(_, _) {},
);
addTearDown(subscription.close);
await container.read(communityMembershipProvider.future);
expect(session.queryCount, 1);

container.invalidate(communityMembershipProvider);
await container.read(communityMembershipProvider.future);
expect(session.queryCount, 2);

session.setStatus(SessionStatus.reconnecting);
await container.pump();
expect(session.queryCount, 2);
session.setStatus(SessionStatus.connected);
await container.pump();
await container.read(communityMembershipProvider.future);
expect(session.queryCount, 3);
});
});
}

class _TestRelayConfigNotifier extends RelayConfigNotifier {
@override
RelayConfig build() =>
const RelayConfig(baseUrl: 'https://relay.example.com');
}

class _CountingRelaySession extends RelaySessionNotifier {
_CountingRelaySession(this.owner);

final String owner;
int queryCount = 0;

@override
SessionState build() => const SessionState(status: SessionStatus.connected);

void setStatus(SessionStatus status) {
state = SessionState(status: status);
}

@override
Future<List<NostrEvent>> queryRelay(
List<NostrFilter> filters, {
Duration timeout = const Duration(seconds: 8),
}) async {
queryCount++;
return [
_event(
createdAt: queryCount,
tags: [
['member', owner, 'owner'],
],
),
];
}
}

NostrEvent _event({required int createdAt, required List<List<String>> tags}) =>
Expand Down
Loading