From 29f7a2c444d60fa155451d7e7f65cf637a1b7f41 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Wed, 25 Mar 2026 17:22:28 +0100
Subject: [PATCH] fix: M1 M2 M6 M7 L3 L5 - subnet batching, scroll debounce, error logging, typing timeout, remove unused deps

---
 windows/flutter/generated_plugins.cmake        |    1 
 windows/flutter/generated_plugin_registrant.cc |    3 +
 lib/providers/providers.dart                   |    4 +
 ios/Podfile.lock                               |   19 ++----
 macos/Flutter/GeneratedPluginRegistrant.swift  |    6 -
 TODO-appstore.md                               |   12 ++--
 lib/services/mqtt_service.dart                 |   29 +++++----
 pubspec.lock                                   |   48 ----------------
 lib/screens/chat_screen.dart                   |   20 +++++-
 pubspec.yaml                                   |    2 
 10 files changed, 55 insertions(+), 89 deletions(-)

diff --git a/TODO-appstore.md b/TODO-appstore.md
index 4f73b46..252f1aa 100644
--- a/TODO-appstore.md
+++ b/TODO-appstore.md
@@ -20,21 +20,21 @@
 
 ## MEDIUM (Improve before submission)
 
-- [ ] **M1: Subnet scan hammers 254 hosts** — Could trigger IDS. Detect subnet mask or cap range
-- [ ] **M2: No loadMore debounce** — Scroll pagination fires repeatedly on iOS bounce. Add isLoading guard
+- [x] **M1: Subnet scan hammers 254 hosts** — Batched in groups of 20 with early exit *(fixed 2026-03-25)*
+- [x] **M2: No loadMore debounce** — Added isLoadingMore guard *(fixed 2026-03-25)*
 - [ ] **M3: NavigateNotifier global singleton** — Mutable static, stale reference risk. Move to Riverpod provider
 - [ ] **M4: Unbounded _seenSeqs set** — O(n log n) eviction. Use FIFO deque instead
 - [ ] **M5: Screenshots bloat iCloud backup** — Store images as files, not base64 in JSON. Or exclude from backup
-- [ ] **M6: Unused import** — `server_config.dart` import in chat_screen.dart suppressed with ignore comment
-- [ ] **M7: Silent error swallowing** — ServerConfig _load() catches all exceptions silently. Add debugPrint
+- [x] **M6: Unused import** — Already removed *(fixed 2026-03-25)*
+- [x] **M7: Silent error swallowing** — Added debugPrint on config load failure *(fixed 2026-03-25)*
 
 ## LOW (Nice-to-haves)
 
 - [ ] **L1: PrivacyInfo.xcprivacy** — Required since 2024 for UserDefaults and FileTimestamp APIs
 - [ ] **L2: Privacy policy URL** — Required for microphone/camera access apps
-- [ ] **L3: Unused dependencies** — Remove `web_socket_channel` and `wakelock_plus` from pubspec.yaml
+- [x] **L3: Unused dependencies** — Removed web_socket_channel and wakelock_plus *(fixed 2026-03-25)*
 - [x] **L4: Unnecessary _http._tcp** — Removed from NSBonjourServices *(fixed 2026-03-25)*
-- [ ] **L5: Typing indicator timeout** — Can get stuck if typing_end missed during background. Auto-clear after 10s
+- [x] **L5: Typing indicator timeout** — Auto-clear after 10s *(fixed 2026-03-25)*
 - [ ] **L6: Version number** — Default 1.0.0+1, set correctly before submission
 - [ ] **L7: App icon** — Verify meets Apple guidelines (no alpha channel, correct sizes)
 
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index b24639a..65029b7 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -2,6 +2,9 @@
   - audioplayers_darwin (0.0.1):
     - Flutter
     - FlutterMacOS
+  - bonsoir_darwin (0.0.1):
+    - Flutter
+    - FlutterMacOS
   - device_info_plus (0.0.1):
     - Flutter
   - DKImagePickerController/Core (4.3.9):
@@ -43,8 +46,6 @@
     - Flutter
   - image_picker_ios (0.0.1):
     - Flutter
-  - package_info_plus (0.4.5):
-    - Flutter
   - permission_handler_apple (9.3.0):
     - Flutter
   - record_ios (1.2.0):
@@ -60,23 +61,20 @@
   - SwiftyGif (5.4.5)
   - vibration (1.7.5):
     - Flutter
-  - wakelock_plus (0.0.1):
-    - Flutter
 
 DEPENDENCIES:
   - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
+  - bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - file_picker (from `.symlinks/plugins/file_picker/ios`)
   - Flutter (from `Flutter`)
   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
-  - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
   - record_ios (from `.symlinks/plugins/record_ios/ios`)
   - share_plus (from `.symlinks/plugins/share_plus/ios`)
   - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
   - vibration (from `.symlinks/plugins/vibration/ios`)
-  - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
 
 SPEC REPOS:
   trunk:
@@ -88,6 +86,8 @@
 EXTERNAL SOURCES:
   audioplayers_darwin:
     :path: ".symlinks/plugins/audioplayers_darwin/darwin"
+  bonsoir_darwin:
+    :path: ".symlinks/plugins/bonsoir_darwin/darwin"
   device_info_plus:
     :path: ".symlinks/plugins/device_info_plus/ios"
   file_picker:
@@ -98,8 +98,6 @@
     :path: ".symlinks/plugins/flutter_secure_storage/ios"
   image_picker_ios:
     :path: ".symlinks/plugins/image_picker_ios/ios"
-  package_info_plus:
-    :path: ".symlinks/plugins/package_info_plus/ios"
   permission_handler_apple:
     :path: ".symlinks/plugins/permission_handler_apple/ios"
   record_ios:
@@ -110,11 +108,10 @@
     :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
   vibration:
     :path: ".symlinks/plugins/vibration/ios"
-  wakelock_plus:
-    :path: ".symlinks/plugins/wakelock_plus/ios"
 
 SPEC CHECKSUMS:
   audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
+  bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
   device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
@@ -122,7 +119,6 @@
   Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
   image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
-  package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
   permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
   record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844
   SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
@@ -130,7 +126,6 @@
   shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
   vibration: 8e2f50fc35bb736f9eecb7dd9f7047fbb6a6e888
-  wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
 
 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
 
diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart
index fce1edf..7254e3f 100644
--- a/lib/providers/providers.dart
+++ b/lib/providers/providers.dart
@@ -39,7 +39,9 @@
       if (json != null) {
         state = ServerConfig.fromJson(jsonDecode(json) as Map<String, dynamic>);
       }
-    } catch (_) {}
+    } catch (e) {
+      debugPrint('ServerConfig load failed: $e');
+    }
   }
 
   Future<void> save(ServerConfig config) async {
diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart
index 3c03072..5017f31 100644
--- a/lib/screens/chat_screen.dart
+++ b/lib/screens/chat_screen.dart
@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:convert';
 import 'dart:io';
 
@@ -63,6 +64,7 @@
   final List<Map<String, dynamic>> _pendingMessages = [];
   final Map<String, List<Message>> _catchUpPending = {};
   List<String>? _cachedSessionOrder;
+  Timer? _typingTimer;
 
   @override
   void initState() {
@@ -132,10 +134,13 @@
     }
   }
 
+  bool _isLoadingMore = false;
   void _onScroll() {
-    if (_scrollController.position.pixels >=
-        _scrollController.position.maxScrollExtent - 100) {
-      ref.read(messagesProvider.notifier).loadMore();
+    if (!_isLoadingMore &&
+        _scrollController.position.pixels >=
+            _scrollController.position.maxScrollExtent - 100) {
+      _isLoadingMore = true;
+      ref.read(messagesProvider.notifier).loadMore().then((_) => _isLoadingMore = false);
     }
   }
 
@@ -247,6 +252,15 @@
         // Strict: only show typing for the ACTIVE session, ignore all others
         if (activeId != null && typingSession == activeId) {
           ref.read(isTypingProvider.notifier).state = typing;
+          // Auto-clear after 10s in case typing_end is missed
+          if (typing) {
+            _typingTimer?.cancel();
+            _typingTimer = Timer(const Duration(seconds: 10), () {
+              if (mounted) ref.read(isTypingProvider.notifier).state = false;
+            });
+          } else {
+            _typingTimer?.cancel();
+          }
         }
       case 'typing_end':
         final endSession = msg['sessionId'] as String?;
diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart
index f0c6d21..7625e7b 100644
--- a/lib/services/mqtt_service.dart
+++ b/lib/services/mqtt_service.dart
@@ -228,19 +228,22 @@
           final subnet = '${parts[0]}.${parts[1]}.${parts[2]}';
           _mqttLog('MQTT: scanning $subnet.0/24 on ${iface.name}');
 
-          // Probe all hosts in parallel — 1s timeout each, runs concurrently
-          final futures = <Future<String?>>[];
-          for (int i = 1; i <= 254; i++) {
-            final probe = '$subnet.$i';
-            if (probe == addr.address) continue; // skip self
-            futures.add(_probeHost(probe, config.port));
-          }
-
-          final results = await Future.wait(futures);
-          final found = results.firstWhere((r) => r != null, orElse: () => null);
-          if (found != null) {
-            _mqttLog('MQTT: subnet scan found broker at $found');
-            return found;
+          // Probe in batches of 20 to avoid flooding the network.
+          // Early exit on first hit.
+          for (int batch = 1; batch <= 254; batch += 20) {
+            final end = (batch + 19).clamp(1, 254);
+            final futures = <Future<String?>>[];
+            for (int i = batch; i <= end; i++) {
+              final probe = '$subnet.$i';
+              if (probe == addr.address) continue;
+              futures.add(_probeHost(probe, config.port));
+            }
+            final results = await Future.wait(futures);
+            final found = results.firstWhere((r) => r != null, orElse: () => null);
+            if (found != null) {
+              _mqttLog('MQTT: subnet scan found broker at $found');
+              return found;
+            }
           }
         }
       }
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 67a6ae9..2bcd467 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -6,25 +6,23 @@
 import Foundation
 
 import audioplayers_darwin
+import bonsoir_darwin
 import device_info_plus
 import file_picker
 import file_selector_macos
 import flutter_secure_storage_macos
-import package_info_plus
 import record_macos
 import share_plus
 import shared_preferences_foundation
-import wakelock_plus
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
+  SwiftBonsoirPlugin.register(with: registry.registrar(forPlugin: "SwiftBonsoirPlugin"))
   DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
   FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
   FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
   FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
-  FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
   RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
   SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
-  WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
 }
diff --git a/pubspec.lock b/pubspec.lock
index 911513c..28a7a73 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -608,22 +608,6 @@
       url: "https://pub.dev"
     source: hosted
     version: "9.3.0"
-  package_info_plus:
-    dependency: transitive
-    description:
-      name: package_info_plus
-      sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
-      url: "https://pub.dev"
-    source: hosted
-    version: "9.0.0"
-  package_info_plus_platform_interface:
-    dependency: transitive
-    description:
-      name: package_info_plus_platform_interface
-      sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.2.1"
   path:
     dependency: transitive
     description:
@@ -1069,22 +1053,6 @@
       url: "https://pub.dev"
     source: hosted
     version: "15.0.2"
-  wakelock_plus:
-    dependency: "direct main"
-    description:
-      name: wakelock_plus
-      sha256: "8b12256f616346910c519a35606fb69b1fe0737c06b6a447c6df43888b097f39"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.5.1"
-  wakelock_plus_platform_interface:
-    dependency: transitive
-    description:
-      name: wakelock_plus_platform_interface
-      sha256: "24b84143787220a403491c2e5de0877fbbb87baf3f0b18a2a988973863db4b03"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.4.0"
   web:
     dependency: transitive
     description:
@@ -1093,22 +1061,6 @@
       url: "https://pub.dev"
     source: hosted
     version: "1.1.1"
-  web_socket:
-    dependency: transitive
-    description:
-      name: web_socket
-      sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
-      url: "https://pub.dev"
-    source: hosted
-    version: "1.0.1"
-  web_socket_channel:
-    dependency: "direct main"
-    description:
-      name: web_socket_channel
-      sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
-      url: "https://pub.dev"
-    source: hosted
-    version: "3.0.3"
   win32:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 10bdb1b..621f726 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -13,7 +13,6 @@
   flutter_riverpod: ^2.6.1
   riverpod_annotation: ^2.6.1
   go_router: ^14.8.1
-  web_socket_channel: ^3.0.2
   path_provider: ^2.1.0
   shared_preferences: ^2.5.3
   record: ^6.2.0
@@ -21,7 +20,6 @@
   permission_handler: ^11.4.0
   image_picker: ^1.1.2
   flutter_secure_storage: ^9.2.4
-  wakelock_plus: ^1.2.8
   vibration: ^2.0.1
   share_plus: ^12.0.1
   udp: ^5.0.3
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 94dc16a..5b79aa2 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -7,6 +7,7 @@
 #include "generated_plugin_registrant.h"
 
 #include <audioplayers_windows/audioplayers_windows_plugin.h>
+#include <bonsoir_windows/bonsoir_windows_plugin_c_api.h>
 #include <file_selector_windows/file_selector_windows.h>
 #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
 #include <permission_handler_windows/permission_handler_windows_plugin.h>
@@ -17,6 +18,8 @@
 void RegisterPlugins(flutter::PluginRegistry* registry) {
   AudioplayersWindowsPluginRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
+  BonsoirWindowsPluginCApiRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi"));
   FileSelectorWindowsRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("FileSelectorWindows"));
   FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index c6f03dd..2d751af 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   audioplayers_windows
+  bonsoir_windows
   file_selector_windows
   flutter_secure_storage_windows
   permission_handler_windows

--
Gitblit v1.3.1