Skip to content

Commit

Permalink
Threads support (#252)
Browse files Browse the repository at this point in the history
  • Loading branch information
Airyzz authored Jun 9, 2024
1 parent a93ea1c commit b2969a3
Show file tree
Hide file tree
Showing 25 changed files with 1,107 additions and 89 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 @@ -9,6 +9,7 @@ import 'package:commet/client/matrix/components/emoticon/matrix_space_emoticon_c
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';
import 'package:commet/client/matrix/components/threads/matrix_threads_component.dart';
import 'package:commet/client/matrix/components/url_preview/matrix_url_preview_component.dart';
import 'package:commet/client/matrix/matrix_client.dart';
import 'package:commet/client/matrix/matrix_room.dart';
Expand All @@ -24,6 +25,7 @@ class ComponentRegistry {
MatrixCommandComponent(client),
MatrixUrlPreviewComponent(client),
MatrixInvitationComponent(client),
MatrixThreadsComponent(client),
];
}

Expand Down
23 changes: 23 additions & 0 deletions commet/lib/client/components/threads/thread_component.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:commet/client/attachment.dart';
import 'package:commet/client/client.dart';
import 'package:commet/client/components/component.dart';

abstract class ThreadsComponent<T extends Client> implements Component<T> {
bool isEventInResponseToThread(TimelineEvent event, Timeline timeline);

bool isHeadOfThread(TimelineEvent event, Timeline timeline);

Future<Timeline?> getThreadTimeline(
{required Timeline roomTimeline, required String threadRootEventId});

Future<TimelineEvent?> sendMessage({
required String threadRootEventId,
required Room room,
String? message,
TimelineEvent? inReplyTo,
TimelineEvent? replaceEvent,
List<ProcessedAttachment>? processedAttachments,
});

TimelineEvent? getFirstReplyToThread(TimelineEvent event, Timeline timeline);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import 'dart:async';

import 'package:commet/client/client.dart';
import 'package:commet/client/components/threads/thread_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/matrix/matrix_timeline_event.dart';

import 'package:matrix/matrix.dart' as matrix;

class MatrixThreadTimeline implements Timeline {
@override
Client client;

@override
late List<TimelineEvent> events;

@override
Room room;

MatrixTimeline mainRoomTimeline;

ThreadsComponent component;

String threadRootId;

@override
StreamController<int> onChange = StreamController.broadcast();

@override
StreamController<int> onEventAdded = StreamController.broadcast();

@override
StreamController<int> onRemove = StreamController.broadcast();

late List<StreamSubscription> subs;

String? nextBatch;
bool finished = false;

Future? nextChunkRequest;

MatrixThreadTimeline({
required this.client,
required this.room,
required this.threadRootId,
required this.mainRoomTimeline,
required this.component,
this.nextBatch,
}) {
subs = [
mainRoomTimeline.onEventAdded.stream.listen(onMainTimelineEventAdded),
mainRoomTimeline.onChange.stream.listen(onMainTimelineEventChanged),
mainRoomTimeline.onRemove.stream.listen(onMainTimelineEventRemoved),
];

events = List.empty(growable: true);
}

Future<List<TimelineEvent>> getThreadEvents(
{int limit = 20, String? nextBatch}) async {
var client = this.client as MatrixClient;
var room = this.room as MatrixRoom;

var mx = client.getMatrixClient();
var data = await mx.request(matrix.RequestType.GET,
"/client/unstable/rooms/${room.identifier}/relations/$threadRootId/m.thread",
query: {
"limit": limit.toString(),
if (nextBatch != null) "from": nextBatch
});

var chunk = List<Map<String, dynamic>>.from(data["chunk"] as Iterable);

var mxevents =
chunk.map((e) => matrix.Event.fromJson(e, room.matrixRoom)).toList();

for (var i = 0; i < mxevents.length; i++) {
var event = mxevents[i];

if (event.type == "m.room.encrypted") {
var decrypted =
await mx.encryption?.decryptRoomEvent(room.identifier, event);
if (decrypted != null) {
mxevents[i] = decrypted;
}
}
}

for (var event in mxevents) {
mainRoomTimeline.matrixTimeline?.addAggregatedEvent(event);
}

var convertedEvents = mxevents
.map((e) => MatrixTimelineEvent(e, mx,
timeline: mainRoomTimeline.matrixTimeline))
.toList();

this.nextBatch = data["next_batch"] as String?;

if (this.nextBatch == null) {
finished = true;
var root = data["original_event"] as Map<String, dynamic>?;
if (root != null) {
var matrixEvent = matrix.Event.fromJson(root, room.matrixRoom);
if (matrixEvent.type == "m.room.encrypted") {
var decrypted = await mx.encryption
?.decryptRoomEvent(room.identifier, matrixEvent);
if (decrypted != null) {
matrixEvent = decrypted;
}
}
var event = MatrixTimelineEvent(matrixEvent, mx);
convertedEvents.add(event);
}
}

return convertedEvents;
}

@override
bool canDeleteEvent(TimelineEvent event) {
return mainRoomTimeline.canDeleteEvent(event);
}

@override
Future<void> close() async {
for (var sub in subs) {
sub.cancel();
}
}

@override
void deleteEvent(TimelineEvent event) {
mainRoomTimeline.deleteEvent(event);
}

@override
Future<TimelineEvent?> fetchEventById(String eventId) {
return mainRoomTimeline.fetchEventById(eventId);
}

@override
Future<TimelineEvent?> fetchEventByIdInternal(String eventId) {
return mainRoomTimeline.fetchEventByIdInternal(eventId);
}

@override
bool hasEvent(String eventId) {
return events.any((element) => element.eventId == eventId);
}

@override
void insertEvent(int index, TimelineEvent event) {}

@override
Future<void> loadMoreHistory() async {
if (finished) {
return;
}

if (nextChunkRequest != null) {
return;
}

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

nextChunkRequest = null;

for (var event in nextEvents) {
events.add(event);
onEventAdded.add(events.length - 1);
}
}

@override
void markAsRead(TimelineEvent event) {}

@override
void notifyChanged(int index) {}

@override
List<String>? get receipts => null;

@override
TimelineEvent? tryGetEvent(String eventId) {
return mainRoomTimeline.tryGetEvent(eventId);
}

bool isEventInThisThread(TimelineEvent event) {
if (event is! MatrixTimelineEvent) {
return false;
}

if (event.eventId == threadRootId) {
return true;
}

var mxEvent = event.event;
var relation = mxEvent.content["m.relates_to"];
if (relation == null) {
return false;
}

if (relation is! Map<String, dynamic>) {
return false;
}

if (relation["rel_type"] != matrix.RelationshipTypes.thread) {
return false;
}

if (relation["event_id"] == threadRootId) {
return true;
}

var reply = relation["m.in_reply_to"] as Map<String, dynamic>?;

if (reply == null) {
return false;
}

var replyingEventID = reply["event_id"];

if (replyingEventID == threadRootId) {
return true;
}

var replyingEvent = mainRoomTimeline.tryGetEvent(replyingEventID);
if (replyingEvent != null) {
return isEventInThisThread(replyingEvent as MatrixTimelineEvent);
}

return false;
}

void onMainTimelineEventAdded(int index) {
var event = mainRoomTimeline.events[index];

if (!isEventInThisThread(event)) {
return;
}

if (index == 0) {
events.insert(0, event);
onEventAdded.add(0);
} else {
// Theres gotta be a smarter way of doing this but whatever
var copy = List<TimelineEvent>.from(mainRoomTimeline.events);
copy.removeWhere((element) => !isEventInThisThread(element));

var newIndex = copy.indexOf(event);
events.insert(newIndex, event);
onEventAdded.add(newIndex);
}
}

void onMainTimelineEventChanged(int index) {
var event = mainRoomTimeline.events[index];
if (isEventInThisThread(event)) {
var index =
events.indexWhere((element) => element.eventId == event.eventId);

events[index] = event;
if (index != -1) {
onChange.add(index);
}
}
}

void onMainTimelineEventRemoved(int index) {
var event = mainRoomTimeline.events[index];
if (isEventInThisThread(event)) {
var index =
events.indexWhere((element) => element.eventId == event.eventId);

events.removeAt(index);

if (index != -1) {
onRemove.add(index);
}
}
}
}
Loading

0 comments on commit b2969a3

Please sign in to comment.