diff --git a/lib/src/room_timeline.dart b/lib/src/room_timeline.dart index 6c55e0f0..da9d5312 100644 --- a/lib/src/room_timeline.dart +++ b/lib/src/room_timeline.dart @@ -598,13 +598,44 @@ class RoomTimeline extends Timeline { } return; } -} -extension on List { - int get firstIndexWhereNotError { - if (isEmpty) return 0; - final index = indexWhere((event) => !event.status.isError); - if (index == -1) return length; - return index; + /// Add an event to the aggregation tree + void addAggregatedEvent(Event event) { + final relationshipType = event.relationshipType; + final relationshipEventId = event.relationshipEventId; + if (relationshipType == null || relationshipEventId == null) { + return; + } + final e = (aggregatedEvents[relationshipEventId] ??= + >{})[relationshipType] ??= {}; + _removeEventFromSet(e, event); + e.add(event); + if (onChange != null) { + final index = _findEvent(event_id: relationshipEventId); + onChange?.call(index); + } + } + + /// Remove an event from aggregation + void removeAggregatedEvent(Event event) { + aggregatedEvents.remove(event.eventId); + if (event.transactionId != null) { + aggregatedEvents.remove(event.transactionId); + } + for (final types in aggregatedEvents.values) { + for (final e in types.values) { + _removeEventFromSet(e, event); + } + } + } + + /// Remove event from set based on event or transaction ID + void _removeEventFromSet(Set eventSet, Event event) { + eventSet.removeWhere( + (e) => + e.matchesEventOrTransactionId(event.eventId) || + event.unsigned != null && + e.matchesEventOrTransactionId(event.transactionId), + ); } } \ No newline at end of file diff --git a/lib/src/thread.dart b/lib/src/thread.dart index 1e09437f..0e9b243b 100644 --- a/lib/src/thread.dart +++ b/lib/src/thread.dart @@ -4,12 +4,14 @@ class Thread { final Room room; final Event rootEvent; Event? lastEvent; + String? prev_batch; final Client client; Thread({ required this.room, required this.rootEvent, required this.client, + this.prev_batch, this.lastEvent, }); diff --git a/lib/src/thread_timeline.dart b/lib/src/thread_timeline.dart index 032057b3..b480aad9 100644 --- a/lib/src/thread_timeline.dart +++ b/lib/src/thread_timeline.dart @@ -1,22 +1,163 @@ +import 'dart:async'; + import 'package:matrix/matrix.dart'; import 'package:matrix/src/models/timeline_chunk.dart'; import 'package:matrix/src/thread.dart'; +// ThreadTimeline: hey RoomTimeline can i copy your homework? +// RoomTimeline: sure just don't make it too obvious +// ThreadTimeline: + class ThreadTimeline extends Timeline { final Thread thread; - + @override List get events => chunk.events; TimelineChunk chunk; + StreamSubscription? timelineSub; + StreamSubscription? historySub; + StreamSubscription? roomSub; + StreamSubscription? sessionIdReceivedSub; + StreamSubscription? cancelSendEventSub; + ThreadTimeline({ required this.thread, - required this.chunk + required this.chunk, + super.onUpdate, + super.onChange, + super.onInsert, + super.onRemove, + super.onNewEvent, }) { - + final room = thread.room; + timelineSub = room.client.onTimelineEvent.stream.listen( + (event) => _handleEventUpdate(event, EventUpdateType.timeline), + ); + historySub = room.client.onHistoryEvent.stream.listen( + (event) => _handleEventUpdate(event, EventUpdateType.history), + ); } + void _handleEventUpdate( + Event event, + EventUpdateType type, { + bool update = true, + }) { + try { + if (event.roomId != thread.room.id) return; + // Ignore events outside of this thread + if (event.relationshipType != RelationshipTypes.thread || + event.relationshipEventId != thread.rootEvent.eventId) return; + + if (type != EventUpdateType.timeline && type != EventUpdateType.history) { + return; + } + + if (type == EventUpdateType.timeline) { + onNewEvent?.call(); + } + + if (type == EventUpdateType.history && + events.indexWhere((e) => e.eventId == event.eventId) != -1) { + return; + } + var index = events.length; + if (type == EventUpdateType.history) { + events.add(event); + } else { + index = events.firstIndexWhereNotError; + events.insert(index, event); + } + onInsert?.call(index); + + addAggregatedEvent(event); + + // Handle redaction events + if (event.type == EventTypes.Redaction) { + final index = _findEvent(event_id: event.redacts); + if (index < events.length) { + removeAggregatedEvent(events[index]); + + // Is the redacted event a reaction? Then update the event this + // belongs to: + if (onChange != null) { + final relationshipEventId = events[index].relationshipEventId; + if (relationshipEventId != null) { + onChange?.call(_findEvent(event_id: relationshipEventId)); + return; + } + } + + events[index].setRedactionEvent(event); + onChange?.call(index); + } + } + + if (update) { + onUpdate?.call(); + } + } catch (e, s) { + Logs().w('Handle event update failed', e, s); + } + } + + /// Add an event to the aggregation tree + void addAggregatedEvent(Event event) { + final relationshipType = event.relationshipType; + final relationshipEventId = event.relationshipEventId; + if (relationshipType == null || relationshipEventId == null) { + return; + } + final e = (aggregatedEvents[relationshipEventId] ??= + >{})[relationshipType] ??= {}; + _removeEventFromSet(e, event); + e.add(event); + if (onChange != null) { + final index = _findEvent(event_id: relationshipEventId); + onChange?.call(index); + } + } + + /// Remove an event from aggregation + void removeAggregatedEvent(Event event) { + aggregatedEvents.remove(event.eventId); + if (event.transactionId != null) { + aggregatedEvents.remove(event.transactionId); + } + for (final types in aggregatedEvents.values) { + for (final e in types.values) { + _removeEventFromSet(e, event); + } + } + } + + /// Remove event from set based on event or transaction ID + void _removeEventFromSet(Set eventSet, Event event) { + eventSet.removeWhere( + (e) => + e.matchesEventOrTransactionId(event.eventId) || + event.unsigned != null && + e.matchesEventOrTransactionId(event.transactionId), + ); + } + + /// Find event index by event ID or transaction ID + int _findEvent({String? event_id, String? unsigned_txid}) { + final searchNeedle = {}; + if (event_id != null) searchNeedle.add(event_id); + if (unsigned_txid != null) searchNeedle.add(unsigned_txid); + + int i; + for (i = 0; i < events.length; i++) { + final searchHaystack = {events[i].eventId}; + final txnid = events[i].transactionId; + if (txnid != null) searchHaystack.add(txnid); + if (searchNeedle.intersection(searchHaystack).isNotEmpty) break; + } + return i; + } @override // TODO: implement canRequestFuture @@ -38,19 +179,22 @@ class ThreadTimeline extends Timeline { } @override - Future requestFuture({int historyCount = Room.defaultHistoryCount, StateFilter? filter}) { + Future requestFuture( + {int historyCount = Room.defaultHistoryCount, StateFilter? filter}) { // TODO: implement requestFuture throw UnimplementedError(); } @override - Future requestHistory({int historyCount = Room.defaultHistoryCount, StateFilter? filter}) { + Future requestHistory( + {int historyCount = Room.defaultHistoryCount, StateFilter? filter}) { // TODO: implement requestHistory throw UnimplementedError(); } @override - void requestKeys({bool tryOnlineBackup = true, bool onlineKeyBackupOnly = true}) { + void requestKeys( + {bool tryOnlineBackup = true, bool onlineKeyBackupOnly = true}) { // TODO: implement requestKeys } @@ -61,9 +205,15 @@ class ThreadTimeline extends Timeline { } @override - Stream<(List, String?)> startSearch({String? searchTerm, int requestHistoryCount = 100, int maxHistoryRequests = 10, String? prevBatch, String? sinceEventId, int? limit, bool Function(Event p1)? searchFunc}) { + Stream<(List, String?)> startSearch( + {String? searchTerm, + int requestHistoryCount = 100, + int maxHistoryRequests = 10, + String? prevBatch, + String? sinceEventId, + int? limit, + bool Function(Event p1)? searchFunc}) { // TODO: implement startSearch throw UnimplementedError(); } - -} \ No newline at end of file +} diff --git a/lib/src/timeline.dart b/lib/src/timeline.dart index 3decd3cb..4219da7b 100644 --- a/lib/src/timeline.dart +++ b/lib/src/timeline.dart @@ -17,11 +17,7 @@ */ import 'dart:async'; -import 'dart:convert'; - -import 'package:collection/collection.dart'; import 'package:matrix/matrix.dart'; -import 'package:matrix/src/models/timeline_chunk.dart'; /// Abstract base class for all timeline implementations. /// Provides common functionality for event management, aggregation, and search. @@ -93,62 +89,6 @@ abstract class Timeline { bool Function(Event)? searchFunc, }); - /// Add an event to the aggregation tree - void addAggregatedEvent(Event event) { - final relationshipType = event.relationshipType; - final relationshipEventId = event.relationshipEventId; - if (relationshipType == null || relationshipEventId == null) { - return; - } - final e = (aggregatedEvents[relationshipEventId] ??= - >{})[relationshipType] ??= {}; - _removeEventFromSet(e, event); - e.add(event); - if (onChange != null) { - final index = _findEvent(event_id: relationshipEventId); - onChange?.call(index); - } - } - - /// Remove an event from aggregation - void removeAggregatedEvent(Event event) { - aggregatedEvents.remove(event.eventId); - if (event.transactionId != null) { - aggregatedEvents.remove(event.transactionId); - } - for (final types in aggregatedEvents.values) { - for (final e in types.values) { - _removeEventFromSet(e, event); - } - } - } - - /// Find event index by event ID or transaction ID - int _findEvent({String? event_id, String? unsigned_txid}) { - final searchNeedle = {}; - if (event_id != null) searchNeedle.add(event_id); - if (unsigned_txid != null) searchNeedle.add(unsigned_txid); - - int i; - for (i = 0; i < events.length; i++) { - final searchHaystack = {events[i].eventId}; - final txnid = events[i].transactionId; - if (txnid != null) searchHaystack.add(txnid); - if (searchNeedle.intersection(searchHaystack).isNotEmpty) break; - } - return i; - } - - /// Remove event from set based on event or transaction ID - void _removeEventFromSet(Set eventSet, Event event) { - eventSet.removeWhere( - (e) => - e.matchesEventOrTransactionId(event.eventId) || - event.unsigned != null && - e.matchesEventOrTransactionId(event.transactionId), - ); - } - /// Handle event updates (to be implemented by subclasses) void _handleEventUpdate(Event event, EventUpdateType type, {bool update = true}); @@ -172,4 +112,14 @@ abstract class Timeline { limit: limit, searchFunc: searchFunc, ).map((result) => result.$1); +} + +// TODO: make up a better name +extension TimelineExtension on List { + int get firstIndexWhereNotError { + if (isEmpty) return 0; + final index = indexWhere((event) => !event.status.isError); + if (index == -1) return length; + return index; + } } \ No newline at end of file