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:
1 parent
fcfd21bb4d
commit
b9cb6a8af1
13 files changed
+926
-322
No files matched your search
@@ -0,0 +1,95 @@
|
||||
import '../../../../api/marianumcloud/search/search_chat_messages_response.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
|
||||
/// One message matching a chat search, either from the loaded history or
|
||||
/// from the server-side search.
|
||||
class ChatSearchHit {
|
||||
final int messageId;
|
||||
final String? actorId;
|
||||
final String? actorDisplayName;
|
||||
final String text;
|
||||
final DateTime? sentAt;
|
||||
final bool isSystem;
|
||||
|
||||
const ChatSearchHit({
|
||||
required this.messageId,
|
||||
required this.text,
|
||||
this.actorId,
|
||||
this.actorDisplayName,
|
||||
this.sentAt,
|
||||
this.isSystem = false,
|
||||
});
|
||||
|
||||
factory ChatSearchHit.fromServer(SearchChatMessagesEntry entry) =>
|
||||
ChatSearchHit(
|
||||
messageId: entry.messageId,
|
||||
text: entry.excerpt,
|
||||
actorId: entry.actorId,
|
||||
actorDisplayName: entry.actorDisplayName,
|
||||
sentAt: entry.sentAt,
|
||||
);
|
||||
}
|
||||
|
||||
class ChatMessageMatcher {
|
||||
/// System messages that are folded into other bubbles (reactions, poll
|
||||
/// votes, deletions) and therefore never rendered nor searched as their own
|
||||
/// entry.
|
||||
static bool isHiddenSystemMessage(GetChatResponseObject element) =>
|
||||
element.systemMessage.contains('reaction') ||
|
||||
element.systemMessage.contains('poll_voted') ||
|
||||
element.systemMessage.contains('message_deleted');
|
||||
|
||||
/// Newest first.
|
||||
static List<ChatSearchHit> findMatches(
|
||||
GetChatResponse response,
|
||||
String query,
|
||||
) {
|
||||
final q = query.trim().toLowerCase();
|
||||
if (q.isEmpty) return const [];
|
||||
|
||||
final matches = <ChatSearchHit>[];
|
||||
for (final element in response.data) {
|
||||
if (element.id <= 0 || isHiddenSystemMessage(element)) continue;
|
||||
|
||||
final text = RichObjectStringProcessor.parseToString(
|
||||
element.message,
|
||||
element.messageParameters,
|
||||
);
|
||||
final isSystem =
|
||||
element.messageType == GetRoomResponseObjectMessageType.system;
|
||||
|
||||
final matched =
|
||||
text.toLowerCase().contains(q) ||
|
||||
(!isSystem && element.actorDisplayName.toLowerCase().contains(q));
|
||||
if (!matched) continue;
|
||||
|
||||
matches.add(
|
||||
ChatSearchHit(
|
||||
messageId: element.id,
|
||||
text: text,
|
||||
actorId: isSystem ? null : element.actorId,
|
||||
actorDisplayName: isSystem ? null : element.actorDisplayName,
|
||||
sentAt: element.sentAt,
|
||||
isSystem: isSystem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
matches.sort((a, b) => b.sentAt!.compareTo(a.sentAt!));
|
||||
return matches;
|
||||
}
|
||||
|
||||
/// Local hits win over server hits for the same message — they carry the
|
||||
/// full text and exact metadata. Result is newest first.
|
||||
static List<ChatSearchHit> merge(
|
||||
List<ChatSearchHit> local,
|
||||
List<ChatSearchHit> server,
|
||||
) {
|
||||
if (server.isEmpty) return local;
|
||||
final seen = {for (final h in local) h.messageId};
|
||||
return [...local, ...server.where((h) => seen.add(h.messageId))]
|
||||
..sort((a, b) => b.messageId.compareTo(a.messageId));
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
|
||||
class ChatSearchMatch {
|
||||
final int messageId;
|
||||
final int timestamp;
|
||||
|
||||
const ChatSearchMatch({required this.messageId, required this.timestamp});
|
||||
}
|
||||
|
||||
class ChatSearchController {
|
||||
/// System messages that are folded into other bubbles (reactions, poll
|
||||
/// votes, deletions) and therefore never rendered nor searched as their own
|
||||
/// entry.
|
||||
static bool isHiddenSystemMessage(GetChatResponseObject element) =>
|
||||
element.systemMessage.contains('reaction') ||
|
||||
element.systemMessage.contains('poll_voted') ||
|
||||
element.systemMessage.contains('message_deleted');
|
||||
|
||||
static List<ChatSearchMatch> findMatches(
|
||||
GetChatResponse response,
|
||||
String query,
|
||||
) {
|
||||
final q = query.trim().toLowerCase();
|
||||
if (q.isEmpty) return const [];
|
||||
|
||||
final matches = <ChatSearchMatch>[];
|
||||
for (final element in response.sortByTimestamp()) {
|
||||
if (isHiddenSystemMessage(element)) continue;
|
||||
|
||||
final haystackText = RichObjectStringProcessor.parseToString(
|
||||
element.message,
|
||||
element.messageParameters,
|
||||
).toLowerCase();
|
||||
|
||||
var matched = haystackText.contains(q);
|
||||
if (!matched &&
|
||||
element.messageType != GetRoomResponseObjectMessageType.system) {
|
||||
matched = element.actorDisplayName.toLowerCase().contains(q);
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
matches.add(
|
||||
ChatSearchMatch(
|
||||
messageId: element.id,
|
||||
timestamp: element.timestamp,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
matches.sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user