Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement timeline search #348

Merged
merged 11 commits into from
Aug 30, 2024
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
Loading