Files
Client/lib/view/pages/talk/search/chat_search_controller.dart
T

130 lines
3.7 KiB
Dart

import 'package:flutter/foundation.dart';
import '../../../../api/demo/demo_mode.dart';
import '../../../../api/marianumcloud/search/search_chat_messages.dart';
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../utils/debouncer.dart';
import '../data/chat_message_matcher.dart';
/// Live state of a search inside one chat, mirroring `FilesSearchController`:
/// hits from the loaded history appear immediately, the server-side search
/// over the full history follows debounced and can be paged further.
class ChatSearchController extends ChangeNotifier {
ChatSearchController({
required this.chatToken,
required GetChatResponse? Function() loadedMessages,
}) : _loadedMessages = loadedMessages;
final String chatToken;
final GetChatResponse? Function() _loadedMessages;
static const Duration _serverDebounce = Duration(seconds: 1);
final String _debounceTag =
'chat-search-${DateTime.now().microsecondsSinceEpoch}';
final SearchChatMessages _api = SearchChatMessages();
String _query = '';
List<ChatSearchHit> _localResults = const [];
List<ChatSearchHit> _serverResults = const [];
Object? _serverCursor;
bool _serverLoading = false;
bool _loadingMore = false;
Object? _serverError;
int _serverEpoch = 0;
bool _disposed = false;
void _safeNotify() {
if (_disposed) return;
notifyListeners();
}
String get query => _query;
bool get serverLoading => _serverLoading;
bool get loadingMore => _loadingMore;
bool get canLoadMore => _serverCursor != null;
Object? get serverError => _serverError;
List<ChatSearchHit> get combinedResults =>
ChatMessageMatcher.merge(_localResults, _serverResults);
void setQuery(String value) {
if (value == _query) return;
_query = value;
// Invalidates in-flight server calls of the previous query.
++_serverEpoch;
_serverResults = const [];
_serverCursor = null;
_loadingMore = false;
_serverError = null;
if (_query.trim().isEmpty) {
Debouncer.cancel(_debounceTag);
_localResults = const [];
_serverLoading = false;
_safeNotify();
return;
}
final loaded = _loadedMessages();
_localResults = loaded == null
? const []
: ChatMessageMatcher.findMatches(loaded, _query);
// Demo sessions must not hit the real Nextcloud (see DemoHttpClient).
_serverLoading = !DemoMode.active;
_safeNotify();
if (_serverLoading) {
Debouncer.debounce(_debounceTag, _serverDebounce, _runServerCall);
}
}
void retry() {
if (_query.trim().isEmpty || DemoMode.active) return;
++_serverEpoch;
Debouncer.cancel(_debounceTag);
_serverResults = const [];
_serverCursor = null;
_loadingMore = false;
_serverLoading = true;
_serverError = null;
_safeNotify();
_runServerCall();
}
void loadMore() {
if (_serverCursor == null || _serverLoading || _loadingMore) return;
_loadingMore = true;
_safeNotify();
_runServerCall(cursor: _serverCursor);
}
Future<void> _runServerCall({Object? cursor}) async {
final epoch = _serverEpoch;
try {
final response = await _api.run(
chatToken: chatToken,
term: _query.trim(),
cursor: cursor,
);
if (epoch != _serverEpoch) return;
_serverResults = [
..._serverResults,
...response.entries.map(ChatSearchHit.fromServer),
];
_serverCursor = response.cursor;
_serverError = null;
} on Object catch (e) {
if (epoch != _serverEpoch) return;
_serverCursor = null;
_serverError = e;
}
_serverLoading = false;
_loadingMore = false;
_safeNotify();
}
@override
void dispose() {
_disposed = true;
Debouncer.cancel(_debounceTag);
super.dispose();
}
}