From 79d9f8ee881eb86deaace9c9c7264fdf5044737e Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 16:32:07 +0200 Subject: [PATCH 01/10] Add network availability notifications when bitchatters are nearby - Show notification when network transitions from empty to having peers - Single notification per session, resets when network becomes empty - Background Bluetooth modes enabled for iOS - Generic message: 'bitchatters nearby\! 1 person around' or 'X people around' --- bitchat.xcodeproj/project.pbxproj | 34 +++++++++++---------- bitchat/Info.plist | 6 ++++ bitchat/Services/BluetoothMeshService.swift | 22 +++++++++++++ bitchat/Services/NotificationService.swift | 10 +++++- bitchat/Views/AppInfoView.swift | 2 +- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index a0a66a6d..5485d760 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -193,18 +193,18 @@ 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatMessageTests.swift; sourceTree = ""; }; 527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchat.entitlements; sourceTree = ""; }; 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = ""; }; - 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.extensionkit-extension"; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 6E2446380E7A44E49A35B664 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = ""; }; 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = ""; }; 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePaddingTests.swift; sourceTree = ""; }; - 8F3A7C058C2C8E1A06C8CF8B /* bitchat_macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; name = bitchat_macOS.app; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 95F16C3A4A5621C74461D8D3 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 96D0D41CA19EE5A772AA8434 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = ""; }; A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = ""; }; AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetryService.swift; sourceTree = ""; }; - C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C1B378C16594575FCC7F9C75 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizedBloomFilter.swift; sourceTree = ""; }; D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = ""; }; @@ -293,7 +293,7 @@ isa = PBXGroup; children = ( 96D0D41CA19EE5A772AA8434 /* bitchat.app */, - 8F3A7C058C2C8E1A06C8CF8B /* bitchat_macOS.app */, + 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */, 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */, C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */, 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */, @@ -392,7 +392,7 @@ packageProductDependencies = ( ); productName = bitchat_macOS; - productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat_macOS.app */; + productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */; productType = "com.apple.product-type.application"; }; 47FF23248747DD7CB666CB91 /* bitchatTests_macOS */ = { @@ -475,7 +475,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1640; TargetAttributes = { 0576A29205865664C0937536 = { DevelopmentTeam = L3N5LHJD5Y; @@ -704,7 +704,6 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGNING_REQUIRED = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = L3N5LHJD5Y; INFOPLIST_FILE = bitchatTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -727,7 +726,6 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGNING_REQUIRED = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = L3N5LHJD5Y; INFOPLIST_FILE = bitchatTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -751,7 +749,7 @@ CODE_SIGNING_REQUIRED = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = L3N5LHJD5Y; + DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = bitchatTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -774,7 +772,6 @@ CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = L3N5LHJD5Y; INFOPLIST_FILE = bitchatShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -799,7 +796,6 @@ CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = L3N5LHJD5Y; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = bitchat; @@ -829,7 +825,7 @@ CODE_SIGNING_REQUIRED = YES; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = L3N5LHJD5Y; + DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = bitchatTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -854,7 +850,6 @@ CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = L3N5LHJD5Y; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = bitchat; @@ -886,7 +881,7 @@ CODE_SIGN_ENTITLEMENTS = "bitchat/bitchat-macOS.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = L3N5LHJD5Y; + DEAD_CODE_STRIPPING = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -896,6 +891,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; + REGISTER_APP_GROUPS = YES; SDKROOT = macosx; SWIFT_VERSION = 5.0; }; @@ -936,9 +932,12 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = L3N5LHJD5Y; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -969,7 +968,7 @@ CODE_SIGN_ENTITLEMENTS = "bitchat/bitchat-macOS.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = L3N5LHJD5Y; + DEAD_CODE_STRIPPING = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -979,6 +978,7 @@ MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; + REGISTER_APP_GROUPS = YES; SDKROOT = macosx; SWIFT_VERSION = 5.0; }; @@ -1019,9 +1019,12 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = L3N5LHJD5Y; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1057,7 +1060,6 @@ CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES; CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = L3N5LHJD5Y; INFOPLIST_FILE = bitchatShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/bitchat/Info.plist b/bitchat/Info.plist index 6e6d9504..fd87f8b5 100644 --- a/bitchat/Info.plist +++ b/bitchat/Info.plist @@ -35,6 +35,12 @@ bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users. NSBluetoothPeripheralUsageDescription bitchat uses Bluetooth to discover and connect with other bitchat users nearby. + UIBackgroundModes + + bluetooth-central + bluetooth-peripheral + remote-notification + UILaunchStoryboardName LaunchScreen UISupportedInterfaceOrientations diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index f798cd4f..d1213923 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -81,6 +81,7 @@ class BluetoothMeshService: NSObject { private let maxTTL: UInt8 = 7 // Maximum hops for long-distance delivery private var announcedToPeers = Set() // Track which peers we've announced to private var announcedPeers = Set() // Track peers who have already been announced + private var hasNotifiedNetworkAvailable = false // Track if we've notified about network availability private var intentionalDisconnects = Set() // Track peripherals we're disconnecting intentionally private var peerLastSeenTimestamps = LRUCache(maxSize: 100) // Bounded cache for peer timestamps private var cleanupTimer: Timer? // Timer to clean up stale peers @@ -599,6 +600,7 @@ class BluetoothMeshService: NSObject { activePeers.removeAll() } announcedPeers.removeAll() + hasNotifiedNetworkAvailable = false // Clear announcement tracking announcedToPeers.removeAll() @@ -1231,6 +1233,7 @@ class BluetoothMeshService: NSObject { peripheralRSSI.removeAll() announcedToPeers.removeAll() announcedPeers.removeAll() + hasNotifiedNetworkAvailable = false processedMessages.removeAll() incomingFragments.removeAll() fragmentMetadata.removeAll() @@ -1331,6 +1334,12 @@ class BluetoothMeshService: NSObject { if !peersToRemove.isEmpty { notifyPeerListUpdate() + + // Reset notification flag if network is now empty + let currentNetworkSize = collectionsQueue.sync { activePeers.count } + if currentNetworkSize == 0 { + hasNotifiedNetworkAvailable = false + } } } @@ -2079,6 +2088,13 @@ class BluetoothMeshService: NSObject { } self.notifyPeerListUpdate(immediate: true) + // Send network available notification if this is the first peer we've seen + let currentNetworkSize = collectionsQueue.sync { self.activePeers.count } + if currentNetworkSize > 0 && !hasNotifiedNetworkAvailable { + hasNotifiedNetworkAvailable = true + NotificationService.shared.sendNetworkAvailableNotification(peerCount: currentNetworkSize) + } + DispatchQueue.main.async { // Check if this is a favorite peer and send notification // Note: This might not work immediately if key exchange hasn't happened yet @@ -2928,6 +2944,12 @@ extension BluetoothMeshService: CBCentralManagerDelegate { DispatchQueue.main.async { self.delegate?.didDisconnectFromPeer(peerID) } + + // Reset notification flag if network is now empty + let currentNetworkSize = collectionsQueue.sync { activePeers.count } + if currentNetworkSize == 0 { + hasNotifiedNetworkAvailable = false + } } self.notifyPeerListUpdate() } diff --git a/bitchat/Services/NotificationService.swift b/bitchat/Services/NotificationService.swift index 61268a7b..74930709 100644 --- a/bitchat/Services/NotificationService.swift +++ b/bitchat/Services/NotificationService.swift @@ -77,10 +77,18 @@ class NotificationService { } func sendFavoriteOnlineNotification(nickname: String) { - let title = "⭐ \(nickname) is online" + let title = "⭐ \(nickname) is online!" let body = "wanna get in there?" let identifier = "favorite-online-\(UUID().uuidString)" sendLocalNotification(title: title, body: body, identifier: identifier) } + + func sendNetworkAvailableNotification(peerCount: Int) { + let title = "👥 bitchatters nearby!" + let body = peerCount == 1 ? "1 person around" : "\(peerCount) people around" + let identifier = "network-available" + + sendLocalNotification(title: title, body: body, identifier: identifier) + } } diff --git a/bitchat/Views/AppInfoView.swift b/bitchat/Views/AppInfoView.swift index f6b673d0..2c651b96 100644 --- a/bitchat/Views/AppInfoView.swift +++ b/bitchat/Views/AppInfoView.swift @@ -18,7 +18,7 @@ struct AppInfoView: View { // MARK: - Constants private enum Strings { - static let appName = "bitchat" + static let appName = "bitchat/" static let tagline = "mesh sidegroupchat" enum Features { From 3194da8f857a4ae8e9615bc5667c5ce97e8ba03a Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 16:35:28 +0200 Subject: [PATCH 02/10] Add anti-spam protection for network availability notifications - 10 minute cooldown between notifications - 5 minute hysteresis before resetting notification flag after network becomes empty - Prevents spam when peers briefly disconnect/reconnect - Emergency disconnect immediately resets all notification state --- bitchat/Services/BluetoothMeshService.swift | 65 +++++++++++++++++---- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index d1213923..4fb0484d 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -82,6 +82,10 @@ class BluetoothMeshService: NSObject { private var announcedToPeers = Set() // Track which peers we've announced to private var announcedPeers = Set() // Track peers who have already been announced private var hasNotifiedNetworkAvailable = false // Track if we've notified about network availability + private var lastNetworkNotificationTime: Date? // Track when we last sent a network notification + private var networkBecameEmptyTime: Date? // Track when the network became empty + private let networkNotificationCooldown: TimeInterval = 600 // 10 minutes between notifications + private let networkEmptyResetDelay: TimeInterval = 300 // 5 minutes before resetting notification flag private var intentionalDisconnects = Set() // Track peripherals we're disconnecting intentionally private var peerLastSeenTimestamps = LRUCache(maxSize: 100) // Bounded cache for peer timestamps private var cleanupTimer: Timer? // Timer to clean up stale peers @@ -600,7 +604,8 @@ class BluetoothMeshService: NSObject { activePeers.removeAll() } announcedPeers.removeAll() - hasNotifiedNetworkAvailable = false + // For normal disconnect, respect the timing + networkBecameEmptyTime = Date() // Clear announcement tracking announcedToPeers.removeAll() @@ -1233,7 +1238,10 @@ class BluetoothMeshService: NSObject { peripheralRSSI.removeAll() announcedToPeers.removeAll() announcedPeers.removeAll() + // For emergency/panic, reset immediately hasNotifiedNetworkAvailable = false + networkBecameEmptyTime = nil + lastNetworkNotificationTime = nil processedMessages.removeAll() incomingFragments.removeAll() fragmentMetadata.removeAll() @@ -1335,10 +1343,27 @@ class BluetoothMeshService: NSObject { if !peersToRemove.isEmpty { notifyPeerListUpdate() - // Reset notification flag if network is now empty + // Mark when network became empty, but don't reset flag immediately + let currentNetworkSize = collectionsQueue.sync { activePeers.count } + if currentNetworkSize == 0 && networkBecameEmptyTime == nil { + networkBecameEmptyTime = Date() + } + } + + // Check if we should reset the notification flag + if let emptyTime = networkBecameEmptyTime { let currentNetworkSize = collectionsQueue.sync { activePeers.count } if currentNetworkSize == 0 { - hasNotifiedNetworkAvailable = false + // Network is still empty, check if enough time has passed + let timeSinceEmpty = Date().timeIntervalSince(emptyTime) + if timeSinceEmpty >= networkEmptyResetDelay { + // Reset the flag after network has been empty for the delay period + hasNotifiedNetworkAvailable = false + // Keep the empty time set so we don't immediately notify again + } + } else { + // Network is no longer empty, clear the empty time + networkBecameEmptyTime = nil } } } @@ -2088,11 +2113,31 @@ class BluetoothMeshService: NSObject { } self.notifyPeerListUpdate(immediate: true) - // Send network available notification if this is the first peer we've seen + // Send network available notification if appropriate let currentNetworkSize = collectionsQueue.sync { self.activePeers.count } - if currentNetworkSize > 0 && !hasNotifiedNetworkAvailable { - hasNotifiedNetworkAvailable = true - NotificationService.shared.sendNetworkAvailableNotification(peerCount: currentNetworkSize) + if currentNetworkSize > 0 { + // Clear empty time since network is active + networkBecameEmptyTime = nil + + if !hasNotifiedNetworkAvailable { + // Check if enough time has passed since last notification + let now = Date() + var shouldSendNotification = true + + if let lastNotification = lastNetworkNotificationTime { + let timeSinceLastNotification = now.timeIntervalSince(lastNotification) + if timeSinceLastNotification < networkNotificationCooldown { + // Too soon to send another notification + shouldSendNotification = false + } + } + + if shouldSendNotification { + hasNotifiedNetworkAvailable = true + lastNetworkNotificationTime = now + NotificationService.shared.sendNetworkAvailableNotification(peerCount: currentNetworkSize) + } + } } DispatchQueue.main.async { @@ -2945,10 +2990,10 @@ extension BluetoothMeshService: CBCentralManagerDelegate { self.delegate?.didDisconnectFromPeer(peerID) } - // Reset notification flag if network is now empty + // Mark when network became empty, but don't reset flag immediately let currentNetworkSize = collectionsQueue.sync { activePeers.count } - if currentNetworkSize == 0 { - hasNotifiedNetworkAvailable = false + if currentNetworkSize == 0 && networkBecameEmptyTime == nil { + networkBecameEmptyTime = Date() } } self.notifyPeerListUpdate() From 537d489daab40e03f156207e258158d27016426f Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 16:39:56 +0200 Subject: [PATCH 03/10] Fix network notifications and add debugging - Network notifications now show even when app is in foreground - Added unique identifiers to prevent iOS deduplication - Set interruptionLevel to timeSensitive for prominence - Added comprehensive logging throughout notification flow - Improved error handling for notification permissions --- bitchat/Services/BluetoothMeshService.swift | 13 +++++++++ bitchat/Services/NotificationService.swift | 31 ++++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 4fb0484d..2db27492 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -1353,16 +1353,19 @@ class BluetoothMeshService: NSObject { // Check if we should reset the notification flag if let emptyTime = networkBecameEmptyTime { let currentNetworkSize = collectionsQueue.sync { activePeers.count } + print("📱 Checking network empty state: size=\(currentNetworkSize), emptyTime=\(emptyTime)") if currentNetworkSize == 0 { // Network is still empty, check if enough time has passed let timeSinceEmpty = Date().timeIntervalSince(emptyTime) if timeSinceEmpty >= networkEmptyResetDelay { // Reset the flag after network has been empty for the delay period + print("📱 Resetting notification flag after \(timeSinceEmpty)s of empty network") hasNotifiedNetworkAvailable = false // Keep the empty time set so we don't immediately notify again } } else { // Network is no longer empty, clear the empty time + print("📱 Network no longer empty, clearing empty time") networkBecameEmptyTime = nil } } @@ -1975,6 +1978,7 @@ class BluetoothMeshService: NSObject { // Check if we've already announced this peer let isFirstAnnounce = !announcedPeers.contains(senderID) + print("📱 Peer announce: \(senderID), isFirstAnnounce: \(isFirstAnnounce), wasInserted: \(wasInserted ?? false)") // Clean up stale peer IDs with the same nickname collectionsQueue.sync(flags: .barrier) { @@ -2115,6 +2119,8 @@ class BluetoothMeshService: NSObject { // Send network available notification if appropriate let currentNetworkSize = collectionsQueue.sync { self.activePeers.count } + print("📱 Network size after peer announce: \(currentNetworkSize), hasNotified: \(hasNotifiedNetworkAvailable)") + if currentNetworkSize > 0 { // Clear empty time since network is active networkBecameEmptyTime = nil @@ -2126,17 +2132,24 @@ class BluetoothMeshService: NSObject { if let lastNotification = lastNetworkNotificationTime { let timeSinceLastNotification = now.timeIntervalSince(lastNotification) + print("📱 Time since last notification: \(timeSinceLastNotification)s, cooldown: \(networkNotificationCooldown)s") if timeSinceLastNotification < networkNotificationCooldown { // Too soon to send another notification shouldSendNotification = false + print("📱 Skipping notification - cooldown period active") } } if shouldSendNotification { + print("📱 Triggering network availability notification for \(currentNetworkSize) peers") hasNotifiedNetworkAvailable = true lastNetworkNotificationTime = now NotificationService.shared.sendNetworkAvailableNotification(peerCount: currentNetworkSize) + } else { + print("📱 Not sending notification - cooldown active") } + } else { + print("📱 Not sending notification - already notified") } } diff --git a/bitchat/Services/NotificationService.swift b/bitchat/Services/NotificationService.swift index 74930709..e76bc932 100644 --- a/bitchat/Services/NotificationService.swift +++ b/bitchat/Services/NotificationService.swift @@ -20,9 +20,11 @@ class NotificationService { private init() {} func requestAuthorization() { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { - // Permission granted + print("📱 Notification permission granted") + } else { + print("📱 Notification permission denied: \(error?.localizedDescription ?? "Unknown")") } } } @@ -87,8 +89,29 @@ class NotificationService { func sendNetworkAvailableNotification(peerCount: Int) { let title = "👥 bitchatters nearby!" let body = peerCount == 1 ? "1 person around" : "\(peerCount) people around" - let identifier = "network-available" + let identifier = "network-available-\(Date().timeIntervalSince1970)" - sendLocalNotification(title: title, body: body, identifier: identifier) + print("📱 Sending network notification: \(body)") + + // For network notifications, we want to show them even in foreground + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = .default + content.interruptionLevel = .timeSensitive // Make it more prominent + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: nil // Deliver immediately + ) + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("📱 Error sending network notification: \(error)") + } else { + print("📱 Network notification sent successfully") + } + } } } From 9f42d919031936163c227f38020e850d3f543da3 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 16:41:43 +0200 Subject: [PATCH 04/10] Fix thread safety issue for notifications - Wrap UIApplication.applicationState access in DispatchQueue.main.async - Network notifications bypass app state check entirely - Added more detailed logging for debugging - Notifications now handled properly from background threads --- bitchat/Services/NotificationService.swift | 99 ++++++++++++---------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/bitchat/Services/NotificationService.swift b/bitchat/Services/NotificationService.swift index e76bc932..6136dc86 100644 --- a/bitchat/Services/NotificationService.swift +++ b/bitchat/Services/NotificationService.swift @@ -30,35 +30,37 @@ class NotificationService { } func sendLocalNotification(title: String, body: String, identifier: String) { - // Check if app is in foreground - #if os(iOS) - guard UIApplication.shared.applicationState != .active else { - // App is active/foreground, skipping notification - return - } - // App state checked, sending notification - #elseif os(macOS) - // On macOS, check if app is active - guard !NSApplication.shared.isActive else { - // App is active/foreground, skipping notification - return - } - // App is not active, sending notification - #endif - - let content = UNMutableNotificationContent() - content.title = title - content.body = body - content.sound = .default - - let request = UNNotificationRequest( - identifier: identifier, - content: content, - trigger: nil // Deliver immediately - ) - - UNUserNotificationCenter.current().add(request) { _ in - // Notification added + // Check if app is in foreground (must be on main thread) + DispatchQueue.main.async { + #if os(iOS) + guard UIApplication.shared.applicationState != .active else { + // App is active/foreground, skipping notification + return + } + // App state checked, sending notification + #elseif os(macOS) + // On macOS, check if app is active + guard !NSApplication.shared.isActive else { + // App is active/foreground, skipping notification + return + } + // App is not active, sending notification + #endif + + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = .default + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: nil // Deliver immediately + ) + + UNUserNotificationCenter.current().add(request) { _ in + // Notification added + } } } @@ -87,6 +89,8 @@ class NotificationService { } func sendNetworkAvailableNotification(peerCount: Int) { + print("📱 sendNetworkAvailableNotification called with peerCount: \(peerCount)") + let title = "👥 bitchatters nearby!" let body = peerCount == 1 ? "1 person around" : "\(peerCount) people around" let identifier = "network-available-\(Date().timeIntervalSince1970)" @@ -94,23 +98,26 @@ class NotificationService { print("📱 Sending network notification: \(body)") // For network notifications, we want to show them even in foreground - let content = UNMutableNotificationContent() - content.title = title - content.body = body - content.sound = .default - content.interruptionLevel = .timeSensitive // Make it more prominent - - let request = UNNotificationRequest( - identifier: identifier, - content: content, - trigger: nil // Deliver immediately - ) - - UNUserNotificationCenter.current().add(request) { error in - if let error = error { - print("📱 Error sending network notification: \(error)") - } else { - print("📱 Network notification sent successfully") + // No app state check - let the notification delegate handle presentation + DispatchQueue.main.async { + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = .default + content.interruptionLevel = .timeSensitive // Make it more prominent + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: nil // Deliver immediately + ) + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("📱 Error sending network notification: \(error)") + } else { + print("📱 Network notification sent successfully") + } } } } From 7b81dd4c0d5fa61eb1a46bd9827a0e8be7e2deb3 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 16:44:34 +0200 Subject: [PATCH 05/10] Fix remaining thread safety issues in notifications - Favorite notifications now bypass sendLocalNotification to avoid thread issues - All notification code properly wrapped in DispatchQueue.main.async - Added logging for favorite notifications - Network and favorite notifications now work independently --- bitchat/Services/NotificationService.swift | 63 ++++++++++++++-------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/bitchat/Services/NotificationService.swift b/bitchat/Services/NotificationService.swift index 6136dc86..3178a04a 100644 --- a/bitchat/Services/NotificationService.swift +++ b/bitchat/Services/NotificationService.swift @@ -30,36 +30,39 @@ class NotificationService { } func sendLocalNotification(title: String, body: String, identifier: String) { - // Check if app is in foreground (must be on main thread) + // Create notification content first (thread-safe) + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = .default + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: nil // Deliver immediately + ) + + // Check app state on main thread and send notification DispatchQueue.main.async { #if os(iOS) guard UIApplication.shared.applicationState != .active else { // App is active/foreground, skipping notification + print("📱 Skipping notification - app is in foreground") return } - // App state checked, sending notification #elseif os(macOS) - // On macOS, check if app is active guard !NSApplication.shared.isActive else { // App is active/foreground, skipping notification + print("📱 Skipping notification - app is in foreground") return } - // App is not active, sending notification #endif - let content = UNMutableNotificationContent() - content.title = title - content.body = body - content.sound = .default - - let request = UNNotificationRequest( - identifier: identifier, - content: content, - trigger: nil // Deliver immediately - ) - - UNUserNotificationCenter.current().add(request) { _ in - // Notification added + // App is in background/inactive, send notification + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("📱 Error sending local notification: \(error)") + } } } } @@ -81,11 +84,29 @@ class NotificationService { } func sendFavoriteOnlineNotification(nickname: String) { - let title = "⭐ \(nickname) is online!" - let body = "wanna get in there?" - let identifier = "favorite-online-\(UUID().uuidString)" + print("📱 sendFavoriteOnlineNotification called for: \(nickname)") - sendLocalNotification(title: title, body: body, identifier: identifier) + // Send directly without checking app state for favorites + DispatchQueue.main.async { + let content = UNMutableNotificationContent() + content.title = "⭐ \(nickname) is online!" + content.body = "wanna get in there?" + content.sound = .default + + let request = UNNotificationRequest( + identifier: "favorite-online-\(UUID().uuidString)", + content: content, + trigger: nil + ) + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("📱 Error sending favorite notification: \(error)") + } else { + print("📱 Favorite notification sent successfully") + } + } + } } func sendNetworkAvailableNotification(peerCount: Int) { From 29c0fdca21aeb11d30c63edb3bc0f6580c5650cc Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 16:46:03 +0200 Subject: [PATCH 06/10] Fix macOS thread checker warning - Move all app state checks inside DispatchQueue.main.async - Ensures NSApplication.isActive is only accessed from main thread - Prevents thread checker warnings on macOS --- bitchat/Services/NotificationService.swift | 27 +++++++++++----------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/bitchat/Services/NotificationService.swift b/bitchat/Services/NotificationService.swift index 3178a04a..c1ea0207 100644 --- a/bitchat/Services/NotificationService.swift +++ b/bitchat/Services/NotificationService.swift @@ -30,19 +30,7 @@ class NotificationService { } func sendLocalNotification(title: String, body: String, identifier: String) { - // Create notification content first (thread-safe) - let content = UNMutableNotificationContent() - content.title = title - content.body = body - content.sound = .default - - let request = UNNotificationRequest( - identifier: identifier, - content: content, - trigger: nil // Deliver immediately - ) - - // Check app state on main thread and send notification + // Everything must be on main thread to check app state DispatchQueue.main.async { #if os(iOS) guard UIApplication.shared.applicationState != .active else { @@ -58,7 +46,18 @@ class NotificationService { } #endif - // App is in background/inactive, send notification + // App is in background/inactive, create and send notification + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = .default + + let request = UNNotificationRequest( + identifier: identifier, + content: content, + trigger: nil // Deliver immediately + ) + UNUserNotificationCenter.current().add(request) { error in if let error = error { print("📱 Error sending local notification: \(error)") From 3d3c7118857d4f438a905a4b84500f2f2c6cdd98 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 16:47:04 +0200 Subject: [PATCH 07/10] Remove app state checks to fix thread issues - Completely remove UIApplication/NSApplication state checks - Let NotificationDelegate handle foreground presentation - All notifications now sent regardless of app state - Fixes all thread checker warnings --- bitchat/Services/NotificationService.swift | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/bitchat/Services/NotificationService.swift b/bitchat/Services/NotificationService.swift index c1ea0207..b5fa4170 100644 --- a/bitchat/Services/NotificationService.swift +++ b/bitchat/Services/NotificationService.swift @@ -30,23 +30,9 @@ class NotificationService { } func sendLocalNotification(title: String, body: String, identifier: String) { - // Everything must be on main thread to check app state + // For now, skip app state check entirely to avoid thread issues + // The NotificationDelegate will handle foreground presentation DispatchQueue.main.async { - #if os(iOS) - guard UIApplication.shared.applicationState != .active else { - // App is active/foreground, skipping notification - print("📱 Skipping notification - app is in foreground") - return - } - #elseif os(macOS) - guard !NSApplication.shared.isActive else { - // App is active/foreground, skipping notification - print("📱 Skipping notification - app is in foreground") - return - } - #endif - - // App is in background/inactive, create and send notification let content = UNMutableNotificationContent() content.title = title content.body = body @@ -61,6 +47,8 @@ class NotificationService { UNUserNotificationCenter.current().add(request) { error in if let error = error { print("📱 Error sending local notification: \(error)") + } else { + print("📱 Local notification sent: \(title)") } } } From f7b4fb815bc1cdd96eb6840dda475be28c333d1f Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 16:50:25 +0200 Subject: [PATCH 08/10] Fix compilation error and add peer tracking logs - Remove reference to undefined wasInserted variable - Add proper logging after wasInserted is defined - Track when peers are added vs already present --- bitchat/Services/BluetoothMeshService.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 2db27492..07ec4351 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -1978,7 +1978,7 @@ class BluetoothMeshService: NSObject { // Check if we've already announced this peer let isFirstAnnounce = !announcedPeers.contains(senderID) - print("📱 Peer announce: \(senderID), isFirstAnnounce: \(isFirstAnnounce), wasInserted: \(wasInserted ?? false)") + print("📱 Peer announce: \(senderID), isFirstAnnounce: \(isFirstAnnounce)") // Clean up stale peer IDs with the same nickname collectionsQueue.sync(flags: .barrier) { @@ -2103,7 +2103,9 @@ class BluetoothMeshService: NSObject { return result } if wasInserted { - // Added peer \(senderID) (\(nickname)) to active peers + print("📱 Added peer \(senderID) (\(nickname)) to active peers") + } else { + print("📱 Peer \(senderID) already in active peers") } // Show join message only for first announce AND if we actually added the peer From 79243c8fcad449fd630b2195c471495118489b12 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 16:53:33 +0200 Subject: [PATCH 09/10] Reduce notification timing for better UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Network empty reset delay: 5 min → 1 min - Notification cooldown: 10 min → 5 min - More responsive when peers drop and rejoin - Still prevents spam from flaky connections --- bitchat/Services/BluetoothMeshService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 07ec4351..f25b4840 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -84,8 +84,8 @@ class BluetoothMeshService: NSObject { private var hasNotifiedNetworkAvailable = false // Track if we've notified about network availability private var lastNetworkNotificationTime: Date? // Track when we last sent a network notification private var networkBecameEmptyTime: Date? // Track when the network became empty - private let networkNotificationCooldown: TimeInterval = 600 // 10 minutes between notifications - private let networkEmptyResetDelay: TimeInterval = 300 // 5 minutes before resetting notification flag + private let networkNotificationCooldown: TimeInterval = 300 // 5 minutes between notifications + private let networkEmptyResetDelay: TimeInterval = 60 // 1 minute before resetting notification flag private var intentionalDisconnects = Set() // Track peripherals we're disconnecting intentionally private var peerLastSeenTimestamps = LRUCache(maxSize: 100) // Bounded cache for peer timestamps private var cleanupTimer: Timer? // Timer to clean up stale peers From b849cfbad3f19ac133ea44fc539b66e86eed4335 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 22 Jul 2025 17:42:53 +0200 Subject: [PATCH 10/10] Remove debug logging from notification system - Clean up all temporary logging statements - Production-ready notification implementation - Network notifications trigger when peers become available --- bitchat/Services/BluetoothMeshService.swift | 17 +---------- bitchat/Services/NotificationService.swift | 34 +++++---------------- 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index f25b4840..29f97f7d 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -1353,19 +1353,16 @@ class BluetoothMeshService: NSObject { // Check if we should reset the notification flag if let emptyTime = networkBecameEmptyTime { let currentNetworkSize = collectionsQueue.sync { activePeers.count } - print("📱 Checking network empty state: size=\(currentNetworkSize), emptyTime=\(emptyTime)") if currentNetworkSize == 0 { // Network is still empty, check if enough time has passed let timeSinceEmpty = Date().timeIntervalSince(emptyTime) if timeSinceEmpty >= networkEmptyResetDelay { // Reset the flag after network has been empty for the delay period - print("📱 Resetting notification flag after \(timeSinceEmpty)s of empty network") hasNotifiedNetworkAvailable = false // Keep the empty time set so we don't immediately notify again } } else { // Network is no longer empty, clear the empty time - print("📱 Network no longer empty, clearing empty time") networkBecameEmptyTime = nil } } @@ -1978,7 +1975,6 @@ class BluetoothMeshService: NSObject { // Check if we've already announced this peer let isFirstAnnounce = !announcedPeers.contains(senderID) - print("📱 Peer announce: \(senderID), isFirstAnnounce: \(isFirstAnnounce)") // Clean up stale peer IDs with the same nickname collectionsQueue.sync(flags: .barrier) { @@ -2103,9 +2099,7 @@ class BluetoothMeshService: NSObject { return result } if wasInserted { - print("📱 Added peer \(senderID) (\(nickname)) to active peers") - } else { - print("📱 Peer \(senderID) already in active peers") + // Added peer \(senderID) (\(nickname)) to active peers } // Show join message only for first announce AND if we actually added the peer @@ -2121,8 +2115,6 @@ class BluetoothMeshService: NSObject { // Send network available notification if appropriate let currentNetworkSize = collectionsQueue.sync { self.activePeers.count } - print("📱 Network size after peer announce: \(currentNetworkSize), hasNotified: \(hasNotifiedNetworkAvailable)") - if currentNetworkSize > 0 { // Clear empty time since network is active networkBecameEmptyTime = nil @@ -2134,24 +2126,17 @@ class BluetoothMeshService: NSObject { if let lastNotification = lastNetworkNotificationTime { let timeSinceLastNotification = now.timeIntervalSince(lastNotification) - print("📱 Time since last notification: \(timeSinceLastNotification)s, cooldown: \(networkNotificationCooldown)s") if timeSinceLastNotification < networkNotificationCooldown { // Too soon to send another notification shouldSendNotification = false - print("📱 Skipping notification - cooldown period active") } } if shouldSendNotification { - print("📱 Triggering network availability notification for \(currentNetworkSize) peers") hasNotifiedNetworkAvailable = true lastNetworkNotificationTime = now NotificationService.shared.sendNetworkAvailableNotification(peerCount: currentNetworkSize) - } else { - print("📱 Not sending notification - cooldown active") } - } else { - print("📱 Not sending notification - already notified") } } diff --git a/bitchat/Services/NotificationService.swift b/bitchat/Services/NotificationService.swift index b5fa4170..4d2cbbfc 100644 --- a/bitchat/Services/NotificationService.swift +++ b/bitchat/Services/NotificationService.swift @@ -22,9 +22,9 @@ class NotificationService { func requestAuthorization() { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { - print("📱 Notification permission granted") + // Permission granted } else { - print("📱 Notification permission denied: \(error?.localizedDescription ?? "Unknown")") + // Permission denied } } } @@ -44,12 +44,8 @@ class NotificationService { trigger: nil // Deliver immediately ) - UNUserNotificationCenter.current().add(request) { error in - if let error = error { - print("📱 Error sending local notification: \(error)") - } else { - print("📱 Local notification sent: \(title)") - } + UNUserNotificationCenter.current().add(request) { _ in + // Notification added } } } @@ -71,8 +67,6 @@ class NotificationService { } func sendFavoriteOnlineNotification(nickname: String) { - print("📱 sendFavoriteOnlineNotification called for: \(nickname)") - // Send directly without checking app state for favorites DispatchQueue.main.async { let content = UNMutableNotificationContent() @@ -86,25 +80,17 @@ class NotificationService { trigger: nil ) - UNUserNotificationCenter.current().add(request) { error in - if let error = error { - print("📱 Error sending favorite notification: \(error)") - } else { - print("📱 Favorite notification sent successfully") - } + UNUserNotificationCenter.current().add(request) { _ in + // Notification added } } } func sendNetworkAvailableNotification(peerCount: Int) { - print("📱 sendNetworkAvailableNotification called with peerCount: \(peerCount)") - let title = "👥 bitchatters nearby!" let body = peerCount == 1 ? "1 person around" : "\(peerCount) people around" let identifier = "network-available-\(Date().timeIntervalSince1970)" - print("📱 Sending network notification: \(body)") - // For network notifications, we want to show them even in foreground // No app state check - let the notification delegate handle presentation DispatchQueue.main.async { @@ -120,12 +106,8 @@ class NotificationService { trigger: nil // Deliver immediately ) - UNUserNotificationCenter.current().add(request) { error in - if let error = error { - print("📱 Error sending network notification: \(error)") - } else { - print("📱 Network notification sent successfully") - } + UNUserNotificationCenter.current().add(request) { _ in + // Notification added } } }