diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index 528ab892b..e83f5fb41 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -102,8 +102,13 @@ class AllPeersLoader { Set _lastQueryOnlineIds = {}; DateTime _lastQueryOnlineTime = DateTime.fromMillisecondsSinceEpoch(0); Timer? _queryOnlineTimer; + List _lastQueryOnlineOptions = const []; + Set _lastOnlineIds = {}; + Set _lastOfflineIds = {}; final Future Function(List ids) _queryOnlines; final Duration _queryOnlineDebounce; + void Function(VoidCallback)? _setState; + bool _isCleared = false; final String _listenerKey = 'AllPeersLoader'; static const String _cbQueryOnlines = 'callback_query_onlines'; @@ -112,8 +117,6 @@ class AllPeersLoader { Duration(milliseconds: 300); static const int _maxQueryOnlineOptions = 20; - late void Function(VoidCallback) setState; - bool get needLoad => !_isPeersLoaded && !_isPeersLoading; bool get isPeersLoaded => _isPeersLoaded; @@ -125,7 +128,8 @@ class AllPeersLoader { queryOnlineDebounce ?? _defaultQueryOnlineDebounce; void init(void Function(VoidCallback) setState) { - this.setState = setState; + _setState = setState; + _isCleared = false; gFFI.recentPeersModel.addListener(_mergeAllPeers); gFFI.lanPeersModel.addListener(_mergeAllPeers); gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); @@ -143,6 +147,9 @@ class AllPeersLoader { gFFI.groupModel.removePeerUpdateListener(_listenerKey); platformFFI.unregisterEventHandler(_cbQueryOnlines, _listenerKey); _queryOnlineTimer?.cancel(); + _lastQueryOnlineOptions = const []; + _setState = null; + _isCleared = true; } Future getAllPeers() async { @@ -169,6 +176,9 @@ class AllPeersLoader { } void _mergeAllPeers() { + if (_isCleared) { + return; + } peers = mergeAutocompletePeers( addressBookPeers: gFFI.abModel.allPeers(), groupPeers: gFFI.groupModel.peers, @@ -176,21 +186,44 @@ class AllPeersLoader { recentPeers: gFFI.recentPeersModel.peers, restRecentPeerIds: gFFI.recentPeersModel.restPeerIds, ); - setState(() { + _applyLastOnlineState(peers); + _scheduleSetState(() { _isPeersLoading = false; _isPeersLoaded = true; }); } void _updateOnlineState(Map evt) { - final changed = updateAutocompletePeerOnlineStates( - peers, - onlines: _splitPeerIds(evt['onlines']), - offlines: _splitPeerIds(evt['offlines']), - ); - if (changed) { - setState(() {}); + if (_isCleared) { + return; } + _lastOnlineIds = _splitPeerIds(evt['onlines']); + _lastOfflineIds = _splitPeerIds(evt['offlines']); + final peersChanged = _applyLastOnlineState(peers); + final optionsChanged = _applyLastOnlineState(_lastQueryOnlineOptions); + if (peersChanged || optionsChanged) { + _scheduleSetState(() {}); + } + } + + void _scheduleSetState(VoidCallback callback) { + if (_isCleared) { + return; + } + final setState = _setState; + if (setState == null) { + callback(); + } else { + setState(callback); + } + } + + bool _applyLastOnlineState(List peers) { + return updateAutocompletePeerOnlineStates( + peers, + onlines: _lastOnlineIds, + offlines: _lastOfflineIds, + ); } Set _splitPeerIds(dynamic ids) { @@ -201,8 +234,12 @@ class AllPeersLoader { } void queryOnlines(Iterable options) { + if (_isCleared) { + return; + } + _lastQueryOnlineOptions = options.toList(growable: false); final ids = autocompleteOnlineQueryIds( - options, + _lastQueryOnlineOptions, limit: _maxQueryOnlineOptions, ).toSet(); _queryOnlineTimer?.cancel(); @@ -219,6 +256,9 @@ class AllPeersLoader { _queryOnlineTimer = Timer(_queryOnlineDebounce, () async { try { await _queryOnlines(ids.toList(growable: false)); + if (_isCleared) { + return; + } _lastQueryOnlineIds = ids; _lastQueryOnlineTime = DateTime.now(); } catch (e) { @@ -226,6 +266,16 @@ class AllPeersLoader { } }); } + + @visibleForTesting + void updateOnlineStateForTesting(Map evt) { + _updateOnlineState(evt); + } + + @visibleForTesting + bool applyLastOnlineStateForTesting(List peers) { + return _applyLastOnlineState(peers); + } } class AutocompletePeerTile extends StatefulWidget { diff --git a/flutter/test/autocomplete_peer_merge_test.dart b/flutter/test/autocomplete_peer_merge_test.dart index f970a03c2..e3a4b318c 100644 --- a/flutter/test/autocomplete_peer_merge_test.dart +++ b/flutter/test/autocomplete_peer_merge_test.dart @@ -115,4 +115,34 @@ void main() { expect(queryCount, 2); }); + + test('online callback updates currently displayed options', () async { + final loader = AllPeersLoader( + queryOnlines: (ids) async {}, + queryOnlineDebounce: Duration(milliseconds: 1), + ); + final displayedOptions = [_peer(id: '123456789')]; + + loader.queryOnlines(displayedOptions); + loader.updateOnlineStateForTesting({ + 'onlines': '123456789', + 'offlines': '', + }); + + expect(displayedOptions.single.online, isTrue); + await Future.delayed(Duration(milliseconds: 2)); + }); + + test('cached online callback state is reapplied after peers merge', () { + final loader = AllPeersLoader(); + loader.updateOnlineStateForTesting({ + 'onlines': '123456789', + 'offlines': '', + }); + + final mergedPeers = [_peer(id: '123456789')]; + loader.applyLastOnlineStateForTesting(mergedPeers); + + expect(mergedPeers.single.online, isTrue); + }); }