mirror of
https://github.com/block/buzz.git
synced 2026-10-28 04:51:40 +00:00
fix(mobile): warn when muted Solo chats are past the push lease limit
A muted Solo chat the lease has no room for loses its mute ignore, so a mention there still notifies. Count those chats, whether dropped by the mute budget or by size trimming, and say so in Notifications settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -25,9 +25,9 @@ class _NotificationsSection extends ConsumerWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final canToggle = hasCapability || community.pushNotificationsEnabled;
|
||||
final omittedSoloChats = community.pushNotificationsEnabled
|
||||
? ref.watch(pushSubscriptionSyncProvider).value ?? 0
|
||||
: 0;
|
||||
final overflow = community.pushNotificationsEnabled
|
||||
? ref.watch(pushSubscriptionSyncProvider).value
|
||||
: null;
|
||||
|
||||
void setEnabled(bool enabled) {
|
||||
unawaited(
|
||||
@@ -75,15 +75,26 @@ class _NotificationsSection extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (omittedSoloChats > 0)
|
||||
if (overflow != null && overflow.chats > 0)
|
||||
AppListRow(
|
||||
key: const ValueKey('push-solo-coverage'),
|
||||
title: 'Older Solo chats notify only on mention',
|
||||
subtitle: omittedSoloChats == 1
|
||||
? '1 chat is past this relay’s notification limit.'
|
||||
: '$omittedSoloChats chats are past this relay’s notification limit.',
|
||||
subtitle: _soloOverflowText(overflow),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _soloOverflowText(SoloOverflow overflow) {
|
||||
final past = overflow.chats == 1
|
||||
? '1 chat is past this relay’s notification limit.'
|
||||
: '${overflow.chats} chats are past this relay’s notification limit.';
|
||||
if (overflow.muted == 0) return past;
|
||||
if (overflow.chats == 1) {
|
||||
return '$past It is muted but still notifies on mention.';
|
||||
}
|
||||
return overflow.muted == 1
|
||||
? '$past 1 of them is muted but still notifies on mention.'
|
||||
: '$past ${overflow.muted} of them are muted but still notify on mention.';
|
||||
}
|
||||
|
||||
@@ -15,25 +15,25 @@ import 'push_setup_recovery.dart';
|
||||
/// the active identity, joined channels, and mute state. This is desired client
|
||||
/// policy only. Relay-accepted authority is introduced by lease publication.
|
||||
///
|
||||
/// Yields how many Solo chats the lease had no room for.
|
||||
final pushSubscriptionSyncProvider = FutureProvider.autoDispose<int>((
|
||||
/// Yields the Solo chats the lease had no room for.
|
||||
final pushSubscriptionSyncProvider = FutureProvider.autoDispose<SoloOverflow>((
|
||||
ref,
|
||||
) async {
|
||||
try {
|
||||
if (ref.watch(authProvider).value?.status != AuthStatus.authenticated) {
|
||||
return 0;
|
||||
return _noOverflow;
|
||||
}
|
||||
final active = ref.watch(activeCommunityProvider).value;
|
||||
final descriptor = ref.watch(currentRelayPushDescriptorProvider).value;
|
||||
if (active == null ||
|
||||
!active.pushNotificationsEnabled ||
|
||||
descriptor == null) {
|
||||
return 0;
|
||||
return _noOverflow;
|
||||
}
|
||||
final channels = ref.watch(channelsProvider).value;
|
||||
final mutes = ref.watch(channelMutesProvider);
|
||||
if (channels == null || !mutes.isReady) {
|
||||
return 0;
|
||||
return _noOverflow;
|
||||
}
|
||||
|
||||
// Legacy Solo chats are listed only in the Solo record. Wait while it
|
||||
@@ -41,7 +41,7 @@ final pushSubscriptionSyncProvider = FutureProvider.autoDispose<int>((
|
||||
// cached copy, keep DMs and mentions current; the Solo tab cannot list
|
||||
// those chats either, and recovery republishes them.
|
||||
final soloRecord = ref.watch(meGroupsProvider);
|
||||
if (soloRecord.isLoading && !soloRecord.hasValue) return 0;
|
||||
if (soloRecord.isLoading && !soloRecord.hasValue) return _noOverflow;
|
||||
|
||||
final desired = desiredBuzzPushSubscriptions(
|
||||
community: active,
|
||||
@@ -53,19 +53,25 @@ final pushSubscriptionSyncProvider = FutureProvider.autoDispose<int>((
|
||||
if (entry.value.muted) entry.key,
|
||||
],
|
||||
);
|
||||
if (desired == null) return 0;
|
||||
if (desired == null) return _noOverflow;
|
||||
await ref
|
||||
.read(communityListProvider.notifier)
|
||||
.updateDesiredPushSubscriptions(active.id, desired.subscriptions);
|
||||
return desired.omittedSoloChats;
|
||||
return desired.overflow;
|
||||
} catch (error, stack) {
|
||||
reportPushSetupError('subscription synchronization', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}, retry: pushSetupRetry);
|
||||
|
||||
/// The desired lease, and how many Solo chats it had no room for.
|
||||
({List<BuzzPushSubscription> subscriptions, int omittedSoloChats})?
|
||||
/// Solo chats a lease had no room for, and how many of those are muted.
|
||||
/// Mentions in a muted one still notify, since its mute ignore did not fit.
|
||||
typedef SoloOverflow = ({int chats, int muted});
|
||||
|
||||
const SoloOverflow _noOverflow = (chats: 0, muted: 0);
|
||||
|
||||
/// The desired lease, and the Solo chats it had no room for.
|
||||
({List<BuzzPushSubscription> subscriptions, SoloOverflow overflow})?
|
||||
desiredBuzzPushSubscriptions({
|
||||
required Community community,
|
||||
required BuzzPushLeaseDescriptor descriptor,
|
||||
@@ -112,9 +118,15 @@ desiredBuzzPushSubscriptions({
|
||||
}
|
||||
fitting = low;
|
||||
}
|
||||
final trimmed = candidates.solos.skip(fitting);
|
||||
return (
|
||||
subscriptions: lease(fitting),
|
||||
omittedSoloChats: candidates.omitted + candidates.solos.length - fitting,
|
||||
overflow: (
|
||||
chats: candidates.omitted + trimmed.length,
|
||||
muted:
|
||||
candidates.omittedMuted +
|
||||
trimmed.where((id) => muted.contains(id.toLowerCase())).length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,7 +136,8 @@ desiredBuzzPushSubscriptions({
|
||||
///
|
||||
/// DMs always stay, as before. Solo chats follow most recent first, within the
|
||||
/// lease's channel counts; the rest push only on mention.
|
||||
({List<String> dms, List<String> solos, int omitted}) _channelWideCandidates({
|
||||
({List<String> dms, List<String> solos, int omitted, int omittedMuted})
|
||||
_channelWideCandidates({
|
||||
required String pubkey,
|
||||
required Iterable<Channel> channels,
|
||||
required Set<String> muted,
|
||||
@@ -149,13 +162,22 @@ desiredBuzzPushSubscriptions({
|
||||
buzzPushMaxChannelWideChannels - dms.where((id) => !isMuted(id)).length;
|
||||
var mutedRoom = buzzPushMaxMutedChannels - dms.where(isMuted).length;
|
||||
final solos = <String>[];
|
||||
var omittedMuted = 0;
|
||||
for (final solo in ordered) {
|
||||
final soloMuted = isMuted(solo.id);
|
||||
if ((soloMuted ? mutedRoom : unmutedRoom) <= 0) continue;
|
||||
if ((soloMuted ? mutedRoom : unmutedRoom) <= 0) {
|
||||
if (soloMuted) omittedMuted++;
|
||||
continue;
|
||||
}
|
||||
solos.add(solo.id);
|
||||
soloMuted ? mutedRoom-- : unmutedRoom--;
|
||||
}
|
||||
return (dms: dms, solos: solos, omitted: ordered.length - solos.length);
|
||||
return (
|
||||
dms: dms,
|
||||
solos: solos,
|
||||
omitted: ordered.length - solos.length,
|
||||
omittedMuted: omittedMuted,
|
||||
);
|
||||
}
|
||||
|
||||
int _mostRecentFirst(Channel a, Channel b) {
|
||||
|
||||
@@ -171,8 +171,10 @@ void main() {
|
||||
savedPrefsProvider.overrideWithValue(prefs),
|
||||
activeCommunityProvider.overrideWith((ref) async => community),
|
||||
currentRelayPushDescriptorProvider.overrideWith((ref) => capability),
|
||||
// The lease had no room for 12 older Solo chats.
|
||||
pushSubscriptionSyncProvider.overrideWith((ref) async => 12),
|
||||
// The lease had no room for 12 older Solo chats, 3 of them muted.
|
||||
pushSubscriptionSyncProvider.overrideWith(
|
||||
(ref) async => (chats: 12, muted: 3),
|
||||
),
|
||||
appLifecycleProvider.overrideWith(_SettingsLifecycleNotifier.new),
|
||||
buzzPushAuthorizationStatusReaderProvider.overrideWithValue(
|
||||
() async => BuzzPushAuthorizationStatus.authorized,
|
||||
@@ -199,7 +201,10 @@ void main() {
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.text('12 chats are past this relay’s notification limit.'),
|
||||
find.text(
|
||||
'12 chats are past this relay’s notification limit. '
|
||||
'3 of them are muted but still notify on mention.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:buzz/features/channels/channel.dart';
|
||||
import 'package:buzz/features/channels/channel_mutes/channel_mutes_provider.dart';
|
||||
@@ -118,7 +119,7 @@ void main() {
|
||||
expect(followed, containsAll([for (var n = 0; n < dmCount; n++) _id(n)]));
|
||||
expect(followed, contains(_id(channels.length - 1)));
|
||||
expect(followed, isNot(contains(_id(dmCount + dmCount - 1))));
|
||||
expect(desired.omittedSoloChats, dmCount);
|
||||
expect(desired.overflow, (chats: dmCount, muted: 0));
|
||||
|
||||
// A relay may advertise any endpoint limit; sizing must not allocate it.
|
||||
expect(
|
||||
@@ -145,6 +146,12 @@ void main() {
|
||||
400,
|
||||
100,
|
||||
),
|
||||
(
|
||||
'content, trimming muted chats,',
|
||||
_descriptor(contentLength: 24000, plaintextLength: 32768),
|
||||
400,
|
||||
300,
|
||||
),
|
||||
('muted channel count', _descriptor(), 400, 400),
|
||||
('NIP-44 plaintext', _descriptor(), 600, 200),
|
||||
]) {
|
||||
@@ -175,8 +182,10 @@ void main() {
|
||||
for (final ignore in desired.subscriptions.first.ignore.skip(1))
|
||||
...?ignore.hTags,
|
||||
};
|
||||
final keptSolos = soloCount - desired.omittedSoloChats;
|
||||
final keptSolos = soloCount - desired.overflow.chats;
|
||||
expect(keptSolos, inInclusiveRange(1, soloCount - 1));
|
||||
// Omitted muted chats lose their mute ignore, so Settings counts them.
|
||||
expect(desired.overflow.muted, math.max(0, mutedSolos - keptSolos));
|
||||
expect(kept, {
|
||||
for (var n = 0; n < dmCount; n++) _id(n),
|
||||
for (var n = channels.length - keptSolos; n < channels.length; n++)
|
||||
|
||||
Reference in New Issue
Block a user