fix(mobile): keep relay sessions stable during push lease updates (#7745)

Enabling push notifications could repeatedly reset the relay session:
reserving a lease generation updates community metadata, which rebuilt
the relay configuration even though the connection identity was
unchanged. This made the avatar and inbox flash and prevented the new
lease from publishing.

Select only the community ID, relay URL, and signing identity as
configuration dependencies. Push bookkeeping keeps the session intact;
real community, URL, and identity changes still notify listeners.

### Validation
The production-provider regression test failed before the fix and passes
afterward; 48 focused mobile tests and static analysis pass.

A personal signed profile build on a physical iPhone published a fresh
lease and received a background notification with the actual message
text. The user confirmed the flashing stopped and tapping the
notification opened the correct community/channel and highlighted the
exact message.

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-09-18 12:10:26 -07:00
committed by GitHub
parent 8953cbfff5
commit 4af2324d85
2 changed files with 92 additions and 6 deletions

View File

@@ -82,12 +82,16 @@ class Env {
class RelayConfigNotifier extends Notifier<RelayConfig> {
@override
RelayConfig build() {
// Watch the active community so that when it changes (community switch),
// the config rebuilds, triggering the full provider cascade.
final activeAsync = ref.watch(activeCommunityProvider);
final active = activeAsync.value;
if (active != null) {
return RelayConfig(baseUrl: active.relayUrl, nsec: active.nsec);
// Push lease bookkeeping updates the community while using its live
// session. Only connection-context changes may restart that session.
final context = ref.watch(
activeCommunityProvider.select((value) {
final active = value.value;
return (id: active?.id, relayUrl: active?.relayUrl, nsec: active?.nsec);
}),
);
if (context.relayUrl != null) {
return RelayConfig(baseUrl: context.relayUrl!, nsec: context.nsec);
}
// Fallback to compile-time env config (dev mode).

View File

@@ -0,0 +1,82 @@
import 'package:buzz/shared/community/community.dart';
import 'package:buzz/shared/community/community_provider.dart';
import 'package:buzz/shared/community/community_storage.dart';
import 'package:buzz/shared/relay/relay_provider.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'community_storage_test.dart';
void main() {
test(
'reserving a push lease does not notify relay config listeners',
() async {
final storage = CommunityStorage(secure: FakeSecureStorage());
final community = Community.create(
name: 'Tom Town',
relayUrl: 'https://relay.example.com',
).copyWith(pushNotificationsEnabled: true);
await storage.save(community);
await storage.saveActiveId(community.id);
final container = ProviderContainer(
overrides: [
communityStorageProvider.overrideWithValue(storage),
communitySnapshotWriterProvider.overrideWithValue((_) async {}),
],
);
addTearDown(container.dispose);
await container.read(activeCommunityProvider.future);
final changes = <RelayConfig>[];
container.listen(relayConfigProvider, (_, next) => changes.add(next));
await container
.read(communityListProvider.notifier)
.reservePushLeaseGeneration(community.id);
await container.read(activeCommunityProvider.future);
await container.pump();
expect(
changes,
isEmpty,
reason: 'Push metadata must not reset the connected relay session',
);
},
);
for (final change in ['community', 'relay', 'identity']) {
test('$change change notifies relay config listeners', () async {
final storage = CommunityStorage(secure: FakeSecureStorage());
final original = Community.create(
name: 'Original',
relayUrl: 'https://relay.example.com',
nsec: 'first',
);
await storage.save(original);
await storage.saveActiveId(original.id);
final container = ProviderContainer(
overrides: [
communityStorageProvider.overrideWithValue(storage),
communitySnapshotWriterProvider.overrideWithValue((_) async {}),
],
);
addTearDown(container.dispose);
await container.read(activeCommunityProvider.future);
final changes = <RelayConfig>[];
container.listen(relayConfigProvider, (_, next) => changes.add(next));
final updated = switch (change) {
'community' => Community.create(
name: 'Other',
relayUrl: original.relayUrl,
nsec: original.nsec,
),
'relay' => original.copyWith(relayUrl: 'https://other.example.com'),
_ => original.copyWith(nsec: 'second'),
};
await storage.save(updated);
await storage.saveActiveId(updated.id);
container.invalidate(communityListProvider);
await container.read(activeCommunityProvider.future);
await container.pump();
expect(changes, hasLength(1));
expect(changes.single.baseUrl, updated.relayUrl);
expect(changes.single.nsec, updated.nsec);
});
}
}