mirror of
https://github.com/block/buzz.git
synced 2026-11-04 00:12:02 +00:00
fix(mobile): retry failed channel searches after reconnecting (#8968)
## Summary Searching while disconnected leaves “Open channel search couldn't finish” visible after the connection returns. Users must change the query or tap Retry channels to recover. This fix automatically retries the open-channel directory search when the relay reconnects, using the current query. It does not rerun message search or explain the separate onboarding setup failure. ### Related issue None found. ### Testing The widget regression searches while disconnected, reconnects without typing, and checks that the error clears and an unjoined open channel appears. It fails on main and passes with this fix. On the iOS simulator at this PR head, restarting the local relay cleared the error within about five seconds without changing the query; the unjoined channel remained listed. ### Screenshots Relay down: the error shows under the cached channel.  Relay back, same query, no typing: the error is gone and the unjoined channel stays listed.  --------- Signed-off-by: John Matthew Tennant <jtennant@squareup.com> Co-authored-by: Simone <0828be588d5c9c4d092c5b1cf01761d5aef63d6b104807fd8e8284d517dd1a99@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
cdd2af1a35
commit
205ff47744
@@ -198,6 +198,15 @@ class SearchNotifier extends Notifier<SearchState> {
|
||||
channelDirectoryLoadStatusProvider,
|
||||
(_, _) => _refreshChannels(),
|
||||
);
|
||||
// Offline, a directory load fails at once. Load it again on reconnect, so
|
||||
// the failure clears without another query.
|
||||
ref.listen(relaySessionProvider, (previous, next) {
|
||||
if (next.status == SessionStatus.connected &&
|
||||
previous?.status != SessionStatus.connected &&
|
||||
_directoryFailed) {
|
||||
retryChannels();
|
||||
}
|
||||
});
|
||||
ref.onDispose(() {
|
||||
_debounce?.cancel();
|
||||
});
|
||||
@@ -261,6 +270,14 @@ class SearchNotifier extends Notifier<SearchState> {
|
||||
_ => true,
|
||||
};
|
||||
|
||||
/// Whether this search loaded the directory for the current query and it
|
||||
/// did not load. An earlier failure stays hidden while the next load runs.
|
||||
bool get _directoryFailed =>
|
||||
_searchesOpenChannels(state.query) &&
|
||||
_directoryAttempted &&
|
||||
_directoryLoad == null &&
|
||||
_directoryMissing;
|
||||
|
||||
/// Loads the directory again after it failed, like desktop's Retry channels.
|
||||
void retryChannels() {
|
||||
_loadDirectory();
|
||||
@@ -376,13 +393,8 @@ class SearchNotifier extends Notifier<SearchState> {
|
||||
ref.read(channelsProvider).asData?.value ?? const <Channel>[],
|
||||
state.query,
|
||||
),
|
||||
// An earlier failure stays hidden while our next load runs, and the
|
||||
// notice clears when another caller loads the directory.
|
||||
channelSearchFailed:
|
||||
_searchesOpenChannels(state.query) &&
|
||||
_directoryAttempted &&
|
||||
_directoryLoad == null &&
|
||||
_directoryMissing,
|
||||
// The notice clears when another caller loads the directory.
|
||||
channelSearchFailed: _directoryFailed,
|
||||
isLoading: _isLoading,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -460,6 +460,44 @@ void main() {
|
||||
expect(spinner, findsOneWidget);
|
||||
expect(find.byKey(const Key('search-channels-failed')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'a reconnect reloads the directory a disconnected search missed',
|
||||
(tester) async {
|
||||
final session = _ReconnectingSession();
|
||||
final actions = _HeldUserSearch();
|
||||
|
||||
await tester.pumpWidget(
|
||||
WidgetHelpers.testable(
|
||||
overrides: [
|
||||
relayConfigProvider.overrideWith(_Community.new),
|
||||
myPubkeyProvider.overrideWith((ref) => 'test'),
|
||||
relaySessionProvider.overrideWith(() => session),
|
||||
channelActionsProvider.overrideWith((ref) => actions),
|
||||
channelsProvider.overrideWith(_NoMemberChannels.new),
|
||||
recentSearchesProvider.overrideWith(_NoRecentSearches.new),
|
||||
profileProvider.overrideWith(_Profile.new),
|
||||
],
|
||||
child: const SearchPage(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Offline, the directory cannot load, and search says so.
|
||||
await tester.enterText(find.byType(TextField), 'ops');
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
session.searches.removeAt(0).complete(const []);
|
||||
actions.searches.removeAt(0).complete(const []);
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('search-channels-failed')), findsOneWidget);
|
||||
|
||||
// Reconnecting loads it for the same query, with no typing.
|
||||
session.connect();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const Key('search-channels-failed')), findsNothing);
|
||||
expect(find.text('ops-room'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Channel _channel(String name, {required bool isMember}) => Channel(
|
||||
@@ -626,3 +664,54 @@ class _MembershipHeldSession extends _HeldSession {
|
||||
return membership.future;
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts offline and answers channel-list reads once [connect]ed, listing
|
||||
/// one open channel, "ops-room".
|
||||
class _ReconnectingSession extends _HeldSession {
|
||||
@override
|
||||
SessionState build() =>
|
||||
const SessionState(status: SessionStatus.disconnected);
|
||||
|
||||
@override
|
||||
Future<void Function()> subscribeWithStatus(
|
||||
NostrFilter filter,
|
||||
void Function(NostrEvent) onEvent, {
|
||||
void Function(String message)? onClosed,
|
||||
required void Function(RelaySubscriptionStatus status) onStatusChanged,
|
||||
}) async {
|
||||
onStatusChanged(RelaySubscriptionStatus.ready);
|
||||
return () {};
|
||||
}
|
||||
|
||||
void connect() => state = const SessionState(status: SessionStatus.connected);
|
||||
|
||||
@override
|
||||
Future<List<NostrEvent>> fetchHistory(
|
||||
NostrFilter filter, {
|
||||
Duration timeout = const Duration(seconds: 8),
|
||||
}) => filter.search == null
|
||||
? Future.value(const [])
|
||||
: super.fetchHistory(filter, timeout: timeout);
|
||||
|
||||
@override
|
||||
Future<List<NostrEvent>> queryRelay(
|
||||
List<NostrFilter> filters, {
|
||||
Duration timeout = const Duration(seconds: 8),
|
||||
}) async => [
|
||||
if (filters.single.kinds.contains(39000) && filters.single.until == null)
|
||||
const NostrEvent(
|
||||
id: 'meta-ops',
|
||||
pubkey: 'relay',
|
||||
createdAt: 2,
|
||||
kind: 39000,
|
||||
tags: [
|
||||
['d', 'ops-id'],
|
||||
['name', 'ops-room'],
|
||||
['t', 'stream'],
|
||||
['public'],
|
||||
],
|
||||
content: '',
|
||||
sig: 'sig',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user