mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
Push subscription setup ran synchronously inside the root widget build, so an invalid subscription could replace the entire app with Flutter's error surface. Push initialization and the cold-start native notification read could also prevent the app from rendering. Keep push work independent of the app subtree, observe asynchronous persistence failures, and retain the original errors and stack traces. Setup retries use bounded backoff; Settings shows a failure and offers manual retry without resetting navigation. Retry preserves a healthy revocation worker, and community-removal callbacks follow a recovered worker so lease cleanup continues after setup recovery. Related: [#8608](https://github.com/block/buzz/pull/8608) fixes the specific unrelated-mute input that exposed this startup coupling. ### Validation Added Flutter journeys cover invalid subscriptions, asynchronous storage failure, dependency initialization failure, retry exhaustion and manual recovery, navigation preservation, accessible retry actions, and pending/failed native startup reads with notification-target recovery. Both original rendering regressions failed before the fix. Community-removal coverage also verifies cleanup after a worker initialization failure and after retrying healthy setup; that regression failed before the worker fix. The full mobile suite passed (3,782 tests; 19 existing skips), as did the four unconfigured-build tests and static analysis. The corrected head also passed the full pre-push mobile checks and tests, plus 42 focused push and community tests. Rendered widget evidence uses mocked native platform channels; no device APNs delivery test was performed. Rendered Flutter widget evidence with synthetic data and mocked native platform views. ### Setup failure The app remains usable and Settings offers a retry after push initialization fails.  ### Recovery Retry clears the error without replacing the navigator or leaving Settings.  <!-- buzz-review-completed --> --------- Signed-off-by: Tom Brow <tomb@block.xyz>
72 lines
2.6 KiB
Dart
72 lines
2.6 KiB
Dart
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
import '../../features/channels/channel.dart';
|
|
import '../../features/channels/channel_mutes/channel_mutes_provider.dart';
|
|
import '../../features/channels/channels_provider.dart';
|
|
import '../auth/auth.dart';
|
|
import '../relay/relay_provider.dart';
|
|
import 'push_subscription.dart';
|
|
import 'push_relay_capability_provider.dart';
|
|
import 'push_setup_recovery.dart';
|
|
|
|
/// Keeps the persisted desired lease and the App Group snapshot aligned with
|
|
/// the active identity, joined channels, and mute state. This is desired client
|
|
/// policy only. Relay-accepted authority is introduced by lease publication.
|
|
final pushSubscriptionSyncProvider = FutureProvider.autoDispose<void>((
|
|
ref,
|
|
) async {
|
|
try {
|
|
if (ref.watch(authProvider).value?.status != AuthStatus.authenticated) {
|
|
return;
|
|
}
|
|
final active = ref.watch(activeCommunityProvider).value;
|
|
if (active == null ||
|
|
!active.pushNotificationsEnabled ||
|
|
ref.watch(currentRelayPushDescriptorProvider).value == null) {
|
|
return;
|
|
}
|
|
final channels = ref.watch(channelsProvider).value;
|
|
final mutes = ref.watch(channelMutesProvider);
|
|
if (channels == null || !mutes.isReady) {
|
|
return;
|
|
}
|
|
|
|
final subscriptions = desiredBuzzPushSubscriptions(
|
|
community: active,
|
|
channels: channels,
|
|
mutedChannelIds: [
|
|
for (final entry in mutes.store.channels.entries)
|
|
if (entry.value.muted) entry.key,
|
|
],
|
|
);
|
|
if (subscriptions == null) return;
|
|
await ref
|
|
.read(communityListProvider.notifier)
|
|
.updateDesiredPushSubscriptions(active.id, subscriptions);
|
|
} catch (error, stack) {
|
|
reportPushSetupError('subscription synchronization', error, stack);
|
|
rethrow;
|
|
}
|
|
}, retry: pushSetupRetry);
|
|
|
|
List<BuzzPushSubscription>? desiredBuzzPushSubscriptions({
|
|
required Community community,
|
|
required Iterable<Channel> channels,
|
|
required Iterable<String> mutedChannelIds,
|
|
}) {
|
|
final pubkey = community.pubkey ?? pubkeyFromNsec(community.nsec);
|
|
if (pubkey == null || pubkey.isEmpty) return null;
|
|
return buildDesiredBuzzPushSubscriptions(
|
|
myPubkey: pubkey,
|
|
channelIds: [
|
|
// Activity surfaces channel-wide traffic only for joined DM channels.
|
|
// Other message kinds enter the inbox through an exact #p mention or
|
|
// participant-thread tag, so subscribing to every joined channel would
|
|
// over-notify compared with the product predicate.
|
|
for (final channel in channels)
|
|
if (channel.isDm && channel.isMember && !channel.isArchived) channel.id,
|
|
],
|
|
mutedChannelIds: mutedChannelIds,
|
|
);
|
|
}
|