130 lines
3.7 KiB
Dart
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();
|
|
}
|
|
}
|