mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
fix(mobile): fit Solo chats within push lease capacity
DMs keep their channel-wide subscription. Own Solo chats fill the remaining lease capacity most recent first, and the rest push only on mention instead of failing the whole lease update. Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -12,6 +12,14 @@ const buzzPushMaxSubscriptions = 16;
|
||||
const buzzPushMaxIgnoreFilters = 8;
|
||||
const buzzPushHellthreadParticipantLimit = 20;
|
||||
|
||||
/// Channels one lease can follow channel-wide after its self-directed
|
||||
/// subscription, and muted channels its ignores can cover after the
|
||||
/// self-authored ignore.
|
||||
const buzzPushMaxChannelWideChannels =
|
||||
(buzzPushMaxSubscriptions - 1) * buzzPushChannelChunkSize;
|
||||
const buzzPushMaxMutedChannels =
|
||||
(buzzPushMaxIgnoreFilters - 1) * buzzPushChannelChunkSize;
|
||||
|
||||
const _supportedNotificationClasses = {'default'};
|
||||
const _filterKeys = {'kinds', 'authors', '#p', '#h', '#e'};
|
||||
final _exactHexPattern = RegExp(r'^[0-9a-f]{64}$');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../features/channels/channel.dart';
|
||||
@@ -68,18 +69,68 @@ List<BuzzPushSubscription>? desiredBuzzPushSubscriptions({
|
||||
if (pubkey == null || pubkey.isEmpty) return null;
|
||||
return buildDesiredBuzzPushSubscriptions(
|
||||
myPubkey: pubkey,
|
||||
channelIds: [
|
||||
// Channel-wide traffic pushes only for joined DMs and the viewer's own
|
||||
// Solo chats, which are conversations with them. Other channels push
|
||||
// through an exact #p mention, so subscribing to every joined channel
|
||||
// would over-notify.
|
||||
for (final channel in channels)
|
||||
if ((channel.isDm ||
|
||||
channel.isPersonalChat(pubkey, legacySoloChannelIds)) &&
|
||||
channel.isMember &&
|
||||
!channel.isArchived)
|
||||
channel.id,
|
||||
],
|
||||
channelIds: _channelWideIds(
|
||||
pubkey: pubkey,
|
||||
channels: channels,
|
||||
mutedChannelIds: mutedChannelIds,
|
||||
legacySoloChannelIds: legacySoloChannelIds,
|
||||
),
|
||||
mutedChannelIds: mutedChannelIds,
|
||||
);
|
||||
}
|
||||
|
||||
/// Channel-wide traffic pushes only for joined DMs and the viewer's own Solo
|
||||
/// chats, which are conversations with them. Other channels push through an
|
||||
/// exact #p mention, so subscribing to every joined channel would over-notify.
|
||||
///
|
||||
/// DMs always fit, as before. Solo chats fill the remaining lease capacity
|
||||
/// most recent first; the rest push only on mention.
|
||||
List<String> _channelWideIds({
|
||||
required String pubkey,
|
||||
required Iterable<Channel> channels,
|
||||
required Iterable<String> mutedChannelIds,
|
||||
required Iterable<String> legacySoloChannelIds,
|
||||
}) {
|
||||
final muted = {for (final id in mutedChannelIds) id.toLowerCase()};
|
||||
bool isMuted(String id) => muted.contains(id.toLowerCase());
|
||||
final joined = [
|
||||
for (final channel in channels)
|
||||
if (channel.isMember && !channel.isArchived) channel,
|
||||
];
|
||||
final ids = [
|
||||
for (final channel in joined)
|
||||
if (channel.isDm) channel.id,
|
||||
];
|
||||
final solos = [
|
||||
for (final channel in joined)
|
||||
if (!channel.isDm && channel.isPersonalChat(pubkey, legacySoloChannelIds))
|
||||
channel,
|
||||
]..sort(_mostRecentFirst);
|
||||
|
||||
var unmutedRoom =
|
||||
buzzPushMaxChannelWideChannels - ids.where((id) => !isMuted(id)).length;
|
||||
var mutedRoom = buzzPushMaxMutedChannels - ids.where(isMuted).length;
|
||||
var omitted = 0;
|
||||
for (final solo in solos) {
|
||||
final soloMuted = isMuted(solo.id);
|
||||
if ((soloMuted ? mutedRoom : unmutedRoom) <= 0) {
|
||||
omitted++;
|
||||
continue;
|
||||
}
|
||||
ids.add(solo.id);
|
||||
soloMuted ? mutedRoom-- : unmutedRoom--;
|
||||
}
|
||||
if (omitted > 0) {
|
||||
debugPrint(
|
||||
'Push lease is full; $omitted older Solo chats push only on mention.',
|
||||
);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
int _mostRecentFirst(Channel a, Channel b) {
|
||||
final byRecency = (b.lastMessageAt ?? b.createdAt).compareTo(
|
||||
a.lastMessageAt ?? a.createdAt,
|
||||
);
|
||||
return byRecency != 0 ? byRecency : a.id.compareTo(b.id);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,58 @@ void main() {
|
||||
expect(subscriptions.last.filter.hTags, [dmID, soloID, legacySoloID]);
|
||||
});
|
||||
|
||||
test('fills lease capacity with the most recent Solo chats', () {
|
||||
final keys = nostr.Keys.generate();
|
||||
String id(int n) =>
|
||||
'123e4567-e89b-42d3-a456-${n.toRadixString(16).padLeft(12, '0')}';
|
||||
Channel solo(int n) => channel(
|
||||
id(n),
|
||||
channelType: 'stream',
|
||||
visibility: 'private',
|
||||
labels: const ['buzz.solo'],
|
||||
createdBy: keys.public,
|
||||
lastMessageAt: DateTime(2026).add(Duration(minutes: n)),
|
||||
);
|
||||
const dmCount = 10;
|
||||
const unmutedSolos = buzzPushMaxChannelWideChannels;
|
||||
const mutedSolos = buzzPushMaxMutedChannels + 5;
|
||||
final channels = [
|
||||
for (var n = 0; n < dmCount; n++) channel(id(n)),
|
||||
for (var n = dmCount; n < dmCount + unmutedSolos + mutedSolos; n++)
|
||||
solo(n),
|
||||
];
|
||||
final muted = [
|
||||
for (var n = dmCount + unmutedSolos; n < channels.length; n++) id(n),
|
||||
];
|
||||
|
||||
final subscriptions = desiredBuzzPushSubscriptions(
|
||||
community: Community.create(
|
||||
name: 'Team',
|
||||
relayUrl: 'https://relay.example.com',
|
||||
nsec: keys.nsec,
|
||||
),
|
||||
channels: channels,
|
||||
mutedChannelIds: muted,
|
||||
)!;
|
||||
|
||||
final followed = {
|
||||
for (final subscription in subscriptions.skip(1))
|
||||
...?subscription.filter.hTags,
|
||||
};
|
||||
final ignoredMutes = {
|
||||
for (final ignore in subscriptions.first.ignore.skip(1)) ...?ignore.hTags,
|
||||
};
|
||||
expect(subscriptions, hasLength(buzzPushMaxSubscriptions));
|
||||
expect(followed, hasLength(buzzPushMaxChannelWideChannels));
|
||||
expect(followed, containsAll([for (var n = 0; n < dmCount; n++) id(n)]));
|
||||
expect(followed, contains(id(dmCount + unmutedSolos - 1)));
|
||||
expect(followed, isNot(contains(id(dmCount + dmCount - 1))));
|
||||
expect(followed, contains(id(dmCount + dmCount)));
|
||||
expect(ignoredMutes, hasLength(buzzPushMaxMutedChannels));
|
||||
expect(ignoredMutes, contains(id(channels.length - 1)));
|
||||
expect(ignoredMutes, isNot(contains(id(dmCount + unmutedSolos))));
|
||||
});
|
||||
|
||||
test('waits for the Solo record but not past its failure', () async {
|
||||
const legacySoloID = '123e4567-e89b-42d3-a456-426614174009';
|
||||
final community = Community.create(
|
||||
@@ -166,6 +218,7 @@ Channel channel(
|
||||
String visibility = 'open',
|
||||
List<String> labels = const [],
|
||||
String createdBy = 'author',
|
||||
DateTime? lastMessageAt,
|
||||
}) => Channel(
|
||||
id: id,
|
||||
name: id,
|
||||
@@ -178,6 +231,7 @@ Channel channel(
|
||||
memberCount: 1,
|
||||
isMember: isMember,
|
||||
archivedAt: archived ? DateTime(2026) : null,
|
||||
lastMessageAt: lastMessageAt,
|
||||
);
|
||||
|
||||
class _RecordingCommunityList extends CommunityListNotifier {
|
||||
|
||||
Reference in New Issue
Block a user