searchEvents method
Performs a search for searchTerm or a more complex searchFunc().
Be aware that the entire room history (limited by limit) will be
downloaded, decrypted and then checked which needs a lot of time. If
there are events left in the timeline you get a nextBatch back which
you can use for the next iteration.
Implementation
Future<({List<Event> events, String? nextBatch, DateTime? searchedUntil})>
searchEvents({
String? searchTerm,
bool Function(Event)? searchFunc,
String? nextBatch,
/// The amount of events requested from the server in one call. Also used
/// to chunk database requests.
int limit = 1000,
/// Other event types are not requested from the server.
Set<String> includeEventTypes = const {
EventTypes.Message,
EventTypes.Encrypted,
},
}) async {
assert(searchTerm != null || searchFunc != null);
searchFunc ??= (event) =>
event.body.toLowerCase().contains(searchTerm!.toLowerCase());
final foundEvents = <Event>[];
// We first check out what we have in database:
if (nextBatch == null) {
List<Event> databaseEvents;
var start = 0;
var timelineComplete = false;
do {
databaseEvents = await client.database.getEventList(
this,
start: start,
limit: limit,
);
start += limit;
foundEvents.addAll(databaseEvents.where(searchFunc));
if (databaseEvents.lastOrNull?.type == EventTypes.RoomCreate) {
timelineComplete = true;
break;
}
} while (databaseEvents.isNotEmpty);
if (timelineComplete) {
// We have the complete timeline locally, so we stop here:
return (
events: foundEvents,
nextBatch: null,
searchedUntil: databaseEvents.lastOrNull?.originServerTs,
);
}
}
final filter = StateFilter(types: includeEventTypes.toList());
nextBatch ??= prev_batch;
// Request `limit` events from server, decrypt and filter them and return.
final historyResult = await client.getRoomEvents(
id,
Direction.b,
from: nextBatch ?? prev_batch,
limit: limit,
filter: jsonEncode(filter.toJson()),
);
final events = historyResult.chunk
.map((matrixEvent) => Event.fromMatrixEvent(matrixEvent, this))
.toList();
/// Attempt decrypting the event, if it fails, load the key and try again once.
/// Returns null if event cannot be decrypted.
Future<Event?> loadKeysAndDecryptEvent(
Event event, {
bool keyAlreadyLoaded = false,
}) async {
final decrypted = await client.encryption!.decryptRoomEvent(
event,
store: false,
updateType: EventUpdateType.history,
);
if (decrypted.type != EventTypes.Encrypted) {
return decrypted;
} else if (!keyAlreadyLoaded) {
final content = event.parsedRoomEncryptedContent;
if (content.sessionId != null) {
await client.encryption!.keyManager.maybeAutoRequest(
id,
content.sessionId!,
content.senderKey,
tryOnlineBackup: true,
onlineKeyBackupOnly: true,
awaitRequest: true,
);
return loadKeysAndDecryptEvent(event, keyAlreadyLoaded: true);
}
}
return null;
}
// Decrypt all events one after another:
for (final (index, event) in events.indexed) {
if (event.type == EventTypes.Encrypted && client.encryption != null) {
final decrypted = await loadKeysAndDecryptEvent(event);
if (decrypted != null) {
events[index] = decrypted;
}
}
}
events.removeWhere((event) => event.type == EventTypes.Encrypted);
foundEvents.addAll(events.where(searchFunc));
return (
events: foundEvents,
nextBatch: historyResult.end,
searchedUntil: historyResult.chunk.lastOrNull?.originServerTs,
);
}