fix: Rate limit iOS peer notifications to prevent flood (#972)

* fix: Rate limit iOS peer notifications to prevent flood

- Remove aggressive formIntersection that cleared recentlySeenPeers
  when peers temporarily dropped, causing them to be treated as "new"
- Add 5-minute cooldown between notifications (aligns with Android)
- Use fixed notification identifier so iOS updates existing notification
  instead of creating new ones
- Only mark peers as seen when notification is sent, so peers arriving
  during cooldown are included in the next notification

Fixes notification spam every 10-30 seconds when peers fluctuate.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Bump version to 1.5.1

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-02-02 09:51:26 -10:00
committed by GitHub
co-authored by jack Claude Opus 4.5
parent 6206184862
commit 90a5e8ba9d
4 changed files with 24 additions and 15 deletions
+4 -4
View File
@@ -558,7 +558,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "$(MARKETING_VERSION)";
MARKETING_VERSION = 1.5.1;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = bitchat;
SDKROOT = iphoneos;
@@ -618,7 +618,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "$(MARKETING_VERSION)";
MARKETING_VERSION = 1.5.1;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = bitchat;
SDKROOT = iphoneos;
@@ -654,7 +654,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)";
MARKETING_VERSION = "$(MARKETING_VERSION)";
MARKETING_VERSION = 1.5.1;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = bitchat;
REGISTER_APP_GROUPS = YES;
@@ -746,7 +746,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)";
MARKETING_VERSION = "$(MARKETING_VERSION)";
MARKETING_VERSION = 1.5.1;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = bitchat;
REGISTER_APP_GROUPS = YES;
+2 -1
View File
@@ -96,7 +96,8 @@ final class NotificationService {
func sendNetworkAvailableNotification(peerCount: Int) {
let title = "👥 bitchatters nearby!"
let body = peerCount == 1 ? "1 person around" : "\(peerCount) people around"
let identifier = "network-available-\(Date().timeIntervalSince1970)"
// Fixed identifier so iOS updates the existing notification instead of creating new ones
let identifier = "network-available"
sendLocalNotification(
title: title,
+1
View File
@@ -21,6 +21,7 @@ enum TransportConfig {
// Timers
static let networkResetGraceSeconds: TimeInterval = 600 // 10 minutes
static let networkNotificationCooldownSeconds: TimeInterval = 300 // 5 minutes
static let basePublicFlushInterval: TimeInterval = 0.08 // ~12.5 fps batching
// BLE duty/announce/connect
+17 -10
View File
@@ -3348,18 +3348,25 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv
self.scheduleNetworkEmptyTimer()
} else {
self.invalidateNetworkEmptyTimer()
// Trim out peers we no longer observe before comparing for new arrivals
self.recentlySeenPeers.formIntersection(meshPeerSet)
// Don't trim recentlySeenPeers here - let timers handle cleanup.
// Trimming immediately causes peers to be treated as "new" when they
// briefly drop and reconnect, triggering notification floods.
let newPeers = meshPeerSet.subtracting(self.recentlySeenPeers)
if !newPeers.isEmpty {
self.lastNetworkNotificationTime = Date()
self.recentlySeenPeers.formUnion(newPeers)
NotificationService.shared.sendNetworkAvailableNotification(peerCount: meshPeers.count)
SecureLogger.info(
"👥 Sent bitchatters nearby notification for \(meshPeers.count) mesh peers (new: \(newPeers.count))",
category: .session
)
// Rate limit: max one notification per 5 minutes
let cooldown = TransportConfig.networkNotificationCooldownSeconds
if Date().timeIntervalSince(self.lastNetworkNotificationTime) >= cooldown {
// Only mark peers as seen when we actually notify about them
// This ensures peers arriving during cooldown will be included in the next notification
self.recentlySeenPeers.formUnion(newPeers)
self.lastNetworkNotificationTime = Date()
NotificationService.shared.sendNetworkAvailableNotification(peerCount: meshPeers.count)
SecureLogger.info(
"👥 Sent bitchatters nearby notification for \(meshPeers.count) mesh peers (new: \(newPeers.count))",
category: .session
)
}
self.scheduleNetworkResetTimer()
}
}