Skip to content

Commit

Permalink
Implement timeline search (#348)
Browse files Browse the repository at this point in the history
  • Loading branch information
Airyzz authored Aug 30, 2024
1 parent ddd6279 commit 5668e7f
Show file tree
Hide file tree
Showing 29 changed files with 1,086 additions and 273 deletions.
2 changes: 2 additions & 0 deletions commet/lib/client/components/component_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:commet/client/matrix/components/emoticon/matrix_emoticon_compone
import 'package:commet/client/matrix/components/emoticon/matrix_emoticon_state_manager.dart';
import 'package:commet/client/matrix/components/emoticon/matrix_room_emoticon_component.dart';
import 'package:commet/client/matrix/components/emoticon/matrix_space_emoticon_component.dart';
import 'package:commet/client/matrix/components/event_search/matrix_event_search_component.dart';
import 'package:commet/client/matrix/components/gif/matrix_gif_component.dart';
import 'package:commet/client/matrix/components/invitation/matrix_invitation_component.dart';
import 'package:commet/client/matrix/components/push_notifications/matrix_push_notification_component.dart';
Expand All @@ -30,6 +31,7 @@ class ComponentRegistry {
MatrixInvitationComponent(client),
MatrixThreadsComponent(client),
MatrixDirectMessagesComponent(client),
MatrixEventSearchComponent(client)
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:commet/client/client.dart';
import 'package:commet/client/components/component.dart';
import 'package:commet/client/timeline_events/timeline_event.dart';

abstract class EventSearchSession {
Stream<List<TimelineEvent>> startSearch(String searchTerm);

bool get currentlySearching;
}

abstract class EventSearchComponent<T extends Client> implements Component<T> {
Future<EventSearchSession> createSearchSession(Room room);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import 'package:commet/client/client.dart';
import 'package:commet/client/components/event_search/event_search_component.dart';
import 'package:commet/client/matrix/matrix_client.dart';
import 'package:commet/client/matrix/matrix_room.dart';
import 'package:commet/client/matrix/matrix_timeline.dart';
import 'package:commet/client/timeline_events/timeline_event.dart';
import 'package:commet/ui/molecules/timeline_events/timeline_view_entry.dart';
import 'package:commet/utils/mime.dart';
// ignore: implementation_imports
import 'package:matrix/src/event.dart';

class MatrixEventSearchSession extends EventSearchSession {
MatrixTimeline timeline;
String? currentSearchTerm;
String? prevBatch;

MatrixEventSearchSession(this.timeline);

@override
bool currentlySearching = false;

bool _requireUrl = false;
bool _requireImage = false;
bool _requireVideo = false;
bool _requireAttachment = false;
String? _requiredType;
String? _requiredSender;

static const String hasLinkString = 'has:link';
static const String hasImageString = 'has:image';
static const String hasVideoString = 'has:video';
static const String hasFileString = 'has:file';

List<String>? _words;

@override
Stream<List<TimelineEvent<Client>>> startSearch(String searchTerm) async* {
currentSearchTerm = searchTerm.toLowerCase();

currentlySearching = true;
_words = currentSearchTerm!.split(' ');

var typeMatch = _words!.where((w) => w.startsWith("type:")).firstOrNull;

if (typeMatch != null) {
_requiredType = typeMatch.split('type:').last;
}

var userMatch = _words!.where((w) => w.startsWith("from:")).firstOrNull;
if (userMatch != null) {
_requiredSender = userMatch.split('from:').last;
}

if (_words!.contains(hasLinkString)) _requireUrl = true;
if (_words!.contains(hasImageString)) _requireImage = true;
if (_words!.contains(hasVideoString)) _requireVideo = true;
if (_words!.contains(hasFileString)) _requireAttachment = true;

_words = _words!
.where((w) =>
[
typeMatch,
hasLinkString,
hasImageString,
hasVideoString,
hasFileString
].contains(w) ==
false)
.toList();

var search = timeline.matrixTimeline!
.startSearch(searchTerm: searchTerm, searchFunc: searchFunc);
List<TimelineEvent<Client>> result = List.empty();
await for (final chunk in search) {
result = chunk.$1
.map((e) => (timeline.room as MatrixRoom).convertEvent(e))
.toList();

Map<String, TimelineEvent> m = {};

for (var event in result) {
var type = TimelineViewEntryState.eventToDisplayType(event);
if (type != TimelineEventWidgetDisplayType.hidden) {
m[event.eventId] = event;
}
}

if (chunk.$2 != null) {
prevBatch = chunk.$2;
}

result = m.values.toList();
result.sort((a, b) => b.originServerTs.compareTo(a.originServerTs));

yield result;
}

currentlySearching = false;
yield result;
}

bool searchFunc(Event event) {
final numMatchingWords = _words!
.where((w) => event.plaintextBody.toLowerCase().contains(w))
.length;

if (_requireAttachment) {
if (event.hasAttachment == false) {
return false;
}
}

if (_requireImage) {
if (!Mime.imageTypes.contains(event.attachmentMimetype)) {
return false;
}
}

if (_requireVideo) {
if (!Mime.videoTypes.contains(event.attachmentMimetype)) {
return false;
}
}

if (_requireUrl) {
if (!(event.plaintextBody.contains("https://") ||
event.plaintextBody.contains("http://"))) {
return false;
}
}

if (_requiredType != null) {
if (event.type != _requiredType && event.messageType != _requiredType) {
return false;
}
}

if (_requiredSender != null) {
if (event.senderId != _requiredSender) {
return false;
}
}

if (numMatchingWords < (_words!.length.toDouble() / 2.0)) {
return false;
}

return true;
}
}

class MatrixEventSearchComponent implements EventSearchComponent<MatrixClient> {
@override
MatrixClient client;

MatrixEventSearchComponent(this.client);

@override
Future<EventSearchSession> createSearchSession(Room room) async {
var timeline = await room.getTimeline();
return MatrixEventSearchSession(timeline as MatrixTimeline);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,32 @@ class MatrixThreadTimeline implements Timeline {
@override
StreamController<int> onRemove = StreamController.broadcast();

final StreamController<void> _loadingStatusChangedController =
StreamController.broadcast();

@override
Stream<void> get onLoadingStatusChanged =>
_loadingStatusChangedController.stream;

late List<StreamSubscription> subs;

String? nextBatch;
bool finished = false;

Future? nextChunkRequest;

@override
bool get canLoadFuture => false;

@override
bool get canLoadHistory => nextBatch != null && nextChunkRequest != null;

@override
bool get isLoadingFuture => false;

@override
bool isLoadingHistory = false;

MatrixThreadTimeline({
required this.client,
required this.room,
Expand Down Expand Up @@ -165,6 +184,8 @@ class MatrixThreadTimeline implements Timeline {
return;
}

isLoadingHistory = true;

nextChunkRequest = getThreadEvents(nextBatch: nextBatch);
var nextEvents = await nextChunkRequest;

Expand All @@ -174,6 +195,13 @@ class MatrixThreadTimeline implements Timeline {
events.add(event);
onEventAdded.add(events.length - 1);
}

isLoadingHistory = false;
}

@override
Future<void> loadMoreFuture() {
throw UnimplementedError();
}

@override
Expand Down
8 changes: 6 additions & 2 deletions commet/lib/client/matrix/matrix_room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ class MatrixRoom extends Room {
TimelineEvent convertEvent(matrix.Event event, {matrix.Timeline? timeline}) {
var c = client as MatrixClient;

if (event.redacted) {
return MatrixTimelineEventUnknown(event, client: c);
}

if (event.type == matrix.EventTypes.Message) {
if (event.relationshipType == "m.replace")
return MatrixTimelineEventEdit(event, client: c);
Expand Down Expand Up @@ -513,9 +517,9 @@ class MatrixRoom extends Room {
}

@override
Future<Timeline> loadTimeline() async {
Future<Timeline> getTimeline({String? contextEventId}) async {
_timeline = MatrixTimeline(client as MatrixClient, this, matrixRoom);
await _timeline!.initTimeline();
await _timeline!.initTimeline(contextEventId: contextEventId);
onTimelineLoaded.add(null);
return _timeline!;
}
Expand Down
44 changes: 38 additions & 6 deletions commet/lib/client/matrix/matrix_timeline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class MatrixTimeline extends Timeline {

late MatrixRoom _room;

final StreamController<void> _loadingStatusChangedController =
StreamController.broadcast();

@override
Stream<void> get onLoadingStatusChanged =>
_loadingStatusChangedController.stream;

matrix.Timeline? get matrixTimeline => _matrixTimeline;

MatrixTimeline(
Expand All @@ -37,14 +44,14 @@ class MatrixTimeline extends Timeline {
}
}

Future<void> initTimeline() async {
Future<void> initTimeline({String? contextEventId}) async {
await (client as MatrixClient).firstSync;

_matrixTimeline = await _matrixRoom.getTimeline(
onInsert: onEventInserted,
onChange: onEventChanged,
onRemove: onEventRemoved,
);
onInsert: onEventInserted,
onChange: onEventChanged,
onRemove: onEventRemoved,
eventContextId: contextEventId);

// This could maybe make load times realllly slow if we have a ton of stuff in the cache?
// Might be better to only convert as many as we would need to display immediately and then convert the rest on demand
Expand Down Expand Up @@ -83,7 +90,32 @@ class MatrixTimeline extends Timeline {
@override
Future<void> loadMoreHistory() async {
if (_matrixTimeline?.canRequestHistory == true) {
return await _matrixTimeline!.requestHistory();
var f = _matrixTimeline!.requestHistory();
_loadingStatusChangedController.add(null);

await f;
}
}

@override
bool get canLoadFuture => _matrixTimeline?.canRequestFuture ?? false;

@override
bool get canLoadHistory => _matrixTimeline?.canRequestHistory ?? false;

@override
bool get isLoadingFuture => _matrixTimeline?.isRequestingFuture ?? false;

@override
bool get isLoadingHistory => _matrixTimeline?.isRequestingHistory ?? false;

@override
Future<void> loadMoreFuture() async {
if (canLoadFuture) {
var f = _matrixTimeline?.requestFuture();

_loadingStatusChangedController.add(null);
await f;
}
}

Expand Down
2 changes: 1 addition & 1 deletion commet/lib/client/room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ abstract class Room {
Color getColorOfUser(String userId);

/// Gets the timeline of a room, loading it if not yet loaded
Future<Timeline> loadTimeline();
Future<Timeline> getTimeline({String contextEventId});

/// Enables end to end encryption in a room
Future<void> enableE2EE();
Expand Down
12 changes: 12 additions & 0 deletions commet/lib/client/timeline.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ abstract class Timeline {

Future<void> loadMoreHistory();

Future<void> loadMoreFuture();

bool get isLoadingHistory;

bool get isLoadingFuture;

bool get canLoadFuture;

bool get canLoadHistory;

Stream<void> get onLoadingStatusChanged;

Future<void> close();

@protected
Expand Down
1 change: 0 additions & 1 deletion commet/lib/debug/log.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:commet/main.dart';
import 'package:commet/utils/notifying_list.dart';
import 'package:commet/utils/text_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

enum LogType { info, debug, error, warning }

Expand Down
1 change: 1 addition & 0 deletions commet/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ class App extends StatelessWidget {
return MaterialApp(
title: 'Commet',
theme: theme,
debugShowCheckedModeBanner: false,
navigatorKey: navigator,
localizationsDelegates: T.localizationsDelegates,
builder: (context, child) => Provider<ClientManager>(
Expand Down
2 changes: 1 addition & 1 deletion commet/lib/ui/atoms/role_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class RoleView extends StatelessWidget {

@override
Widget build(BuildContext context) {
var bg = Theme.of(context).colorScheme.surfaceContainerHigh;
var bg = Theme.of(context).colorScheme.surfaceContainerLow;
var fg = Theme.of(context).colorScheme.onSurface;

return Padding(
Expand Down
Loading

0 comments on commit 5668e7f

Please sign in to comment.