chat search now lists local and server-side hits like the file search, and jumping to old messages pages in history with a progress banner and keeps the hit marked

This commit is contained in:
MineTec committed 2026-10-28 22:40:21 +01:00
1 parent fcfd21bb4d
commit b9cb6a8af1
13 files changed
+926 -322

No files matched your search

@@ -1,7 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumcloud/search/search_chat_messages_response.dart';
import 'package:marianum_mobile/api/marianumcloud/talk/chat/get_chat_response.dart';
import 'package:marianum_mobile/api/marianumcloud/talk/room/get_room_response.dart';
import 'package:marianum_mobile/view/pages/talk/data/chat_search_controller.dart';
import 'package:marianum_mobile/view/pages/talk/data/chat_message_matcher.dart';
GetChatResponseObject _msg({
required int id,
@@ -12,36 +13,35 @@ GetChatResponseObject _msg({
GetRoomResponseObjectMessageType type =
GetRoomResponseObjectMessageType.comment,
Map<String, RichObjectString>? params,
}) =>
GetChatResponseObject(
id,
'token',
GetRoomResponseObjectMessageActorType.user,
'actor-id',
actorDisplayName,
timestamp,
systemMessage,
type,
true,
'',
message,
params,
null,
null,
null,
);
}) => GetChatResponseObject(
id,
'token',
GetRoomResponseObjectMessageActorType.user,
'actor-id',
actorDisplayName,
timestamp,
systemMessage,
type,
true,
'',
message,
params,
null,
null,
null,
);
GetChatResponse _response(List<GetChatResponseObject> messages) =>
GetChatResponse(messages.toSet());
void main() {
group('ChatSearchController.findMatches', () {
group('ChatMessageMatcher.findMatches', () {
test('empty query returns no matches', () {
final response = _response([
_msg(id: 1, timestamp: 100, message: 'Hallo'),
]);
expect(ChatSearchController.findMatches(response, ''), isEmpty);
expect(ChatSearchController.findMatches(response, ' '), isEmpty);
expect(ChatMessageMatcher.findMatches(response, ''), isEmpty);
expect(ChatMessageMatcher.findMatches(response, ' '), isEmpty);
});
test('matches message text case-insensitively', () {
@@ -49,7 +49,7 @@ void main() {
_msg(id: 1, timestamp: 100, message: 'Hallo Welt'),
_msg(id: 2, timestamp: 200, message: 'nichts hier'),
]);
final matches = ChatSearchController.findMatches(response, 'WELT');
final matches = ChatMessageMatcher.findMatches(response, 'WELT');
expect(matches.length, 1);
expect(matches.first.messageId, 1);
});
@@ -69,7 +69,7 @@ void main() {
message: 'auch was',
),
]);
final matches = ChatSearchController.findMatches(response, 'lisa');
final matches = ChatMessageMatcher.findMatches(response, 'lisa');
expect(matches.length, 1);
expect(matches.first.messageId, 1);
});
@@ -85,10 +85,7 @@ void main() {
),
]);
// Match on text content
expect(
ChatSearchController.findMatches(response, 'beigetreten').length,
1,
);
expect(ChatMessageMatcher.findMatches(response, 'beigetreten').length, 1);
// Actor name alone (not in text) should not match for system messages
final actorOnlyResponse = _response([
_msg(
@@ -100,7 +97,7 @@ void main() {
),
]);
expect(
ChatSearchController.findMatches(actorOnlyResponse, 'lisa'),
ChatMessageMatcher.findMatches(actorOnlyResponse, 'lisa'),
isEmpty,
);
});
@@ -132,7 +129,7 @@ void main() {
),
_msg(id: 3, timestamp: 300, message: 'Treffer'),
]);
final matches = ChatSearchController.findMatches(response, 'Treffer');
final matches = ChatMessageMatcher.findMatches(response, 'Treffer');
expect(matches.length, 1);
expect(matches.first.messageId, 3);
},
@@ -155,7 +152,7 @@ void main() {
},
),
]);
final matches = ChatSearchController.findMatches(response, 'hausaufgab');
final matches = ChatMessageMatcher.findMatches(response, 'hausaufgab');
expect(matches.length, 1);
expect(matches.first.messageId, 1);
});
@@ -166,8 +163,65 @@ void main() {
_msg(id: 2, timestamp: 300, message: 'X'),
_msg(id: 3, timestamp: 200, message: 'X'),
]);
final matches = ChatSearchController.findMatches(response, 'x');
final matches = ChatMessageMatcher.findMatches(response, 'x');
expect(matches.map((m) => m.messageId).toList(), [2, 3, 1]);
});
});
group('ChatMessageMatcher.merge', () {
test('dedupes by id preferring local hits, newest id first', () {
const local = [ChatSearchHit(messageId: 5, text: 'lokal')];
const server = [
ChatSearchHit(messageId: 9, text: 'neu'),
ChatSearchHit(messageId: 5, text: 'server'),
ChatSearchHit(messageId: 2, text: 'alt'),
];
final merged = ChatMessageMatcher.merge(local, server);
expect(merged.map((h) => h.messageId).toList(), [9, 5, 2]);
expect(merged[1].text, 'lokal');
});
});
group('SearchChatMessagesEntry.fromJson', () {
test('reads id and timestamp from attributes', () {
final entry = SearchChatMessagesEntry.fromJson({
'title': 'Lisa Maier',
'subline': 'Die Hausaufgabe ist bis Freitag',
'attributes': {
'messageId': '123',
'actorId': 'lmaier',
'timestamp': '1700000000',
},
}, term: 'hausaufgabe')!;
expect(entry.messageId, 123);
expect(entry.actorId, 'lmaier');
expect(entry.actorDisplayName, 'Lisa Maier');
expect(entry.excerpt, 'Die Hausaufgabe ist bis Freitag');
expect(entry.sentAt, DateTime.fromMillisecondsSinceEpoch(1700000000000));
});
test('detects excerpt in title and falls back to resourceUrl id', () {
final entry = SearchChatMessagesEntry.fromJson({
'title': '…die Hausaufgabe',
'subline': 'Lisa Maier',
'resourceUrl': 'https://cloud/index.php/call/abc#message_77',
}, term: 'hausaufgabe')!;
expect(entry.messageId, 77);
expect(entry.excerpt, '…die Hausaufgabe');
expect(entry.actorDisplayName, 'Lisa Maier');
expect(entry.sentAt, isNull);
});
test('entries without message id are dropped', () {
final response = SearchChatMessagesResponse.fromJson({
'isPaginated': true,
'cursor': 25,
'entries': [
{'title': 'x', 'subline': 'y'},
],
}, term: 'y');
expect(response.entries, isEmpty);
expect(response.cursor, isNull);
});
});
}